@patchstack/connect 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-INSTALL.md +142 -8
- package/README.md +94 -3
- package/dist/{chunk-3G2I6QL6.js → chunk-IOODP4ZB.js} +19 -2
- package/dist/chunk-IOODP4ZB.js.map +1 -0
- package/dist/cli.js +1257 -196
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protect/runtime/report-listeners.cjs +360 -0
- package/dist/protect/templates/express-guard.cjs +30 -12
- package/dist/protect/templates/express-guard.js +30 -12
- package/dist/protect/templates/express-guard.ts +35 -12
- package/dist/protect/templates/fastify-plugin.cjs +14 -1
- package/dist/protect/templates/fastify-plugin.js +14 -1
- package/dist/protect/templates/fastify-plugin.ts +14 -1
- package/dist/protect/templates/generic-guard.cjs +36 -12
- package/dist/protect/templates/generic-guard.js +36 -12
- package/dist/protect/templates/generic-guard.ts +41 -12
- package/dist/protect.cjs +215 -23
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +40 -0
- package/dist/protect.d.ts +40 -0
- package/dist/protect.edge.js +202 -22
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +192 -25
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2MMPQN2A.js → refresh-manifest-3QGLYXV5.js} +2 -2
- package/dist/{refresh-manifest-2MMPQN2A.js.map → refresh-manifest-3QGLYXV5.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3G2I6QL6.js.map +0 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -8,9 +8,9 @@ Every command at a glance — what it does, whether it reads your source, what i
|
|
|
8
8
|
|
|
9
9
|
| Command | What it does | Reads your source? | Writes to your project | Sends over the network |
|
|
10
10
|
|---|---|---|---|---|
|
|
11
|
-
| `scan` | Provision (or reuse) the site and POST the dependency list for vulnerability matching. Also runs automatically via `setup` and the install/build hooks. | No — lockfile only; `node_modules/` is enumerated when no lockfile can be read (e.g. `bun.lockb`) or when the lockfiles present disagree. It also reads the `<title>` of the root `index.html` and the `name` in `package.json`, to report what the site is called | `.patchstackrc.json` (public: site UUID + settings); `.patchstackrc.local.json` (the API key, created owner-only) and a `.gitignore` entry for it — the CLI says so if it could not add one; the widget `<script>` tag in the root HTML shell — only after a successful post; the production marker in a code root shell — before the post, since it needs no site UUID | Package names + versions; this site's public address and name, where the project or build environment states them |
|
|
11
|
+
| `scan` | Provision (or reuse) the site and POST the dependency list for vulnerability matching. Also runs automatically via `setup` and the install/build hooks. | No source analysis — lockfile only; `node_modules/` is enumerated when no lockfile can be read (e.g. `bun.lockb`) or when the lockfiles present disagree. It also reads the `<title>` of the root `index.html` and the `name` in `package.json`, to report what the site is called. During `prebuild` only, it reads the scaffolded guard and its co-located rules JSON to remove a previous map stamp. | `.patchstackrc.json` (public: site UUID + settings); `.patchstackrc.local.json` (the API key, created owner-only) and a `.gitignore` entry for it — the CLI says so if it could not add one; during `prebuild`, removal of a previous `_patchstack.build_id` from the guard's own rules file so a later build cannot carry stale coordinates; the widget `<script>` tag in the root HTML shell — only after a successful post; the production marker in a code root shell — before the post, since it needs no site UUID | Package names + versions; this site's public address and name, where the project or build environment states them |
|
|
12
12
|
| `setup` | One bounded command: `scan` → manage the widget → install + verify `protect` → wire the install/build scans. Never runs the project build. | No | Config, widget tag, production marker, guard files, `package.json` scripts | Package names + versions and the site's public address and name (via `scan`); a claim token as a request header, only when you pass one |
|
|
13
|
-
| `map` | Local
|
|
13
|
+
| `map` | Local attack-surface analysis (entry points → inputs → sinks → evidence-backed flows). Never run by another command. | **Yes** — via the app's own TypeScript; a pre-bundle `--upload` also locates the co-located rules file imported by the scaffolded guard | Only the file named by `--out`; during a pre-bundle `--upload`, `_patchstack.build_id` in that existing rules file | Nothing — **unless `--upload`**: structure only (routes, parameter names, the package behind each sink, file:line) plus a SHA-256 identity derived from its policy content. Never source code or env values |
|
|
14
14
|
| `protect` | Install the always-on runtime guard; auto-wire known stacks, or scaffold a generic guard + print a wiring plan. `--check` verifies the guard is wired (exit 1 if not); `--demo` seeds a broad sample rule set. Runs automatically **only** via `setup` — never by `scan`, `guide`, `status`, or `mark-build`. | No — writes guard files, does not analyze your code | Guard/framework files (e.g. `middleware.ts`, `src/patchstack/`) | Nothing |
|
|
15
15
|
| `demo node-serialize` | Production-backed walkthrough: confirm the vulnerable package is present, scan, wait for live rule `18843`, install + verify the guard, print test requests. Does not install the package or start/restart the app. | No | Same files as `scan` + `protect` | `scan` payload; polls the public Pulse rules endpoint (never the printed test requests) |
|
|
16
16
|
| `demo-guide node-serialize` | Read-only companion: explains the prepare/run/prove/cleanup sequence and prints the next command. | No | Nothing | Nothing |
|
|
@@ -18,10 +18,11 @@ Every command at a glance — what it does, whether it reads your source, what i
|
|
|
18
18
|
| `status` | Re-print the site UUID + dashboard URL and check whether the site still exists (active / removed / could not verify). | No | Nothing | Site-existence check |
|
|
19
19
|
| `init <site-uuid>` | Optional: pre-seed `.patchstackrc.json` with an existing UUID. | No | `.patchstackrc.json` only | Nothing |
|
|
20
20
|
| `mark-build` | Stamp built HTML with a production flag + build fingerprint and ensure the widget tag in built pages. Run as a `postbuild` step. | No | Build output only (`dist/ build/ out/ .output/public`) — never source | Nothing |
|
|
21
|
+
| `claim` | Attach the site to a Patchstack account from the terminal: print a link the user opens to sign in (or sign up) and poll (10 min). Whoever approves becomes the owner. Does **not** rotate the credential. Same result as opening the dashboard link `scan` prints. Not usable in CI. | No | Nothing, unless the server issues a credential for a checkout that had none — then `.patchstackrc.local.json` | Device-code request + approval poll |
|
|
21
22
|
| `login` | Recover a lost credential for an existing site: print an owner-approval link and poll (10 min). Approving **rotates** the credential. Not usable in CI. | No | New credential into `.patchstackrc.local.json` on approval | Device-code request + approval poll |
|
|
22
23
|
| `uninstall` | Signal Patchstack that the package is being removed: an unclaimed record is deleted, a claimed one is flagged. Does **not** touch local files. | No | Nothing local | Removal signal |
|
|
23
24
|
|
|
24
|
-
Only `map`
|
|
25
|
+
Only `map` analyses your source, and only `map --upload` sends anything derived from it. `scan` additionally reads two declarations the project makes about itself — the `<title>` in the root `index.html` and the `name` in `package.json` — to report what the site is called; during `prebuild` it also reads the scaffolded guard and its co-located rules JSON solely to remove a previous map stamp. `scan` transmits package names + versions, plus the site's own public address and name where the project states them — never source code, file paths, git history, or any environment variable value other than the published URL of this site. `scan --install-paths` additionally sends where each package sits in the dependency tree; it is off unless you pass it.
|
|
25
26
|
|
|
26
27
|
## Package and command behavior
|
|
27
28
|
|
|
@@ -33,15 +34,15 @@ Only `map` reads your source, and only `map --upload` sends anything derived fro
|
|
|
33
34
|
- **Only `scan` looks for the address and the name.** They are resolved in the one code path that reports them, so `guide`, `status`, `login`, `uninstall`, `mark-build`, `init`, `protect`, `demo-guide` and `map` neither read the host's URL variables nor open `index.html` or `package.json` for this. `setup` and `demo` do, because both run `scan`.
|
|
34
35
|
- **The address is the one your visitors use, and, apart from the tool's own `PATCHSTACK_*` settings, it is the only env var value read.** A site provisioned by a scan from a developer machine has no address, so the dashboard shows a placeholder and Patchstack cannot check that the published page still carries what was scanned. `scan` therefore sends `url` when — and only when — it can know it: `url` in `.patchstackrc.json` or `PATCHSTACK_SITE_URL` if you set one, otherwise the single variable a host publishes to name its own **production** URL (`VERCEL_PROJECT_PRODUCTION_URL` on a Vercel production deployment, Netlify's `URL` in the production context, `RENDER_EXTERNAL_URL`, `RAILWAY_PUBLIC_DOMAIN` in a production environment). Preview and branch deployments are excluded, as are hosts that publish no production signal. An address that is not how the public reaches a website is dropped: any IP address (in either family, however it is written), any single-label host such as `localhost` or `production`, and the reserved suffixes (`.local`, `.internal`, `.test`, `.invalid`, `.home.arpa`, …). A `url` you set explicitly that fails those checks is refused with an error rather than replaced by a guess. When nothing qualifies, `url` is omitted from the payload rather than guessed. Patchstack only ever applies it to a site that still has no address; it never re-points a site whose address is already real.
|
|
35
36
|
- **The name is read from your project, never from the host environment.** `name` in `.patchstackrc.json` (or `PATCHSTACK_SITE_NAME`) if you set one; otherwise the `<title>` of the project's root `index.html` (`public/index.html` if there is no root one), read from the file as text — a title your app sets from script is not seen; otherwise the `name` in `package.json`, unless it is a template placeholder such as `vite_react_shadcn_ts`. It is omitted when nothing qualifies, and it only ever fills in a site that has no name yet — a name set in the dashboard is never replaced.
|
|
36
|
-
- **
|
|
37
|
-
- **`scan` makes up to
|
|
37
|
+
- **Only `map` analyses source files.** It parses your server source to report your app's attack surface. It runs only when you invoke it and prints to stdout. It transmits nothing unless you explicitly pass `--upload`, which sends that description of your app's structure to your own site's Patchstack endpoint — never source code, and never without that flag. A `prebuild` scan reads the scaffolded guard and rules JSON only to identify and clear the reserved map stamp; it does not analyse them or transmit their contents.
|
|
38
|
+
- **`scan` makes up to three source edits:** the disclosure widget's `<script>` tag, the production marker, and — during `prebuild` only — removal of a previous `_patchstack.build_id` from the existing guard rules file. None runs on `--dry-run`; all are idempotent. `"widget": false` disables the first two, while stale-stamp removal is independent because it prevents old coordinates being attributed to a new build.
|
|
38
39
|
- The **widget tag** goes in the root HTML shell — the first of `index.html`, `public/index.html`, or `src/app.html` that exists — and only after a successful post, because it carries the site UUID.
|
|
39
40
|
- The **production marker** goes in a root shell that is JSX rather than HTML (e.g. `src/routes/__root.tsx`, `app/layout.tsx`), inside a `{/* #region patchstack */}` block placed above the widget tag. It is written *before* the post: it carries no site UUID and needs no network, and build scripts commonly chain `patchstack-connect scan || true`, where waiting on the server would mean an offline build silently ships without the flag. The marker is guarded by the framework's own production expression (`import.meta.env.PROD`, or `process.env.NODE_ENV === 'production'`), so it is inert in dev and preview builds. Without it a server-rendered site has no built HTML for `mark-build` to stamp, and the widget treats the published site as build mode. `mark-build` writes to build output only (`dist/`, `build/`, `out/`, `.output/public`), never to source. `guide`, `status`, and `init` write nothing except `init`'s own `.patchstackrc.json`.
|
|
40
41
|
- **`setup` runs `scan`, then `protect`, then edits `package.json` scripts:** provisioning happens first so the runtime guard can bake the real site UUID. It verifies the resulting framework seam, preserves existing commands, adds `scan` after dependency installs and before builds, adds `mark-build` after builds, and uses a direct build chain for Bun. It never runs the project build. If the widget or runtime guard needs a framework-specific manual merge, it prints the exact remaining step instead of overwriting user code.
|
|
41
42
|
- The package also exposes **`protect`** directly (runtime exploit guard; its templates live under `dist/protect/`). `setup` invokes it automatically; `scan`, `guide`, `status`, and `mark-build` do not. It writes only local files and auto-wires known stacks — **TanStack Start + Supabase** (patches the Supabase client + `src/start.ts`), **Next.js** (scaffolds `middleware.ts`), **SvelteKit** (`src/hooks.server.ts`), **Astro** (`src/middleware.ts`), **Nuxt** (`server/middleware/`), **NestJS** (`app.use(patchstackMiddleware)` in the bootstrap), **Fastify** (`app.register(patchstackFastify)`), and **Express** (`app.use(patchstackMiddleware)`). On **any other stack** it scaffolds a framework-agnostic guard under `src/patchstack/` and prints a wiring plan — then you finish the install by importing that guard into your server entry (`protectFetch(handler)` for a Web-Fetch server, or `app.use(patchstackMiddleware)` for Node/Express) and running `patchstack-connect protect --check` to confirm it is wired (exit 1 until it is). Passing `--demo` seeds a broad sample rule set (for demonstrations, not production).
|
|
42
43
|
- **`demo node-serialize` is an explicit production-backed walkthrough.** It requires `node-serialize@0.0.4` to already be present in the lockfile; it does not install the vulnerable dependency. It runs the same production `scan`, polls the configured site's public Pulse rules endpoint until rule `18843` is served, runs `protect`, verifies the generated guard, and prints exploit/benign test requests. It writes the same manifest/widget and guard files as those underlying commands. It does not start/restart the app and does not send the printed requests.
|
|
43
|
-
- **`map` is
|
|
44
|
-
- **`map --upload` is the only command that sends a description of your source.** (The runtime guard can also report rule detections, which carry route paths and parameter names — see "Runtime guard reporting" below.) It POSTs the same JSON document to `monitor/pulse/input-map/<your site uuid>` so Patchstack can pin protection rules to your app's own parameter names instead of guessing them.
|
|
44
|
+
- **`map` is local unless you pass `--upload`.** It walks the project's server source (skipping `node_modules`, build output and dot-directories; it does not follow symlinks out of the project unless you pass `--follow-symlinks`), parses it with the project's **own** `typescript`, and prints JSON describing the attack surface: entry points, the inputs each reads, the sinks they can reach (database / file system / process / outbound HTTP) with the npm package behind each, and evidence-backed input→sink flows, each labelled with how the link was established — from an exact read at the sink's own call site, through a transformed or cross-module link, down to the two being present together with no proven link. Static analysis is best-effort, so the output reports the *detected* surface with coverage counters — not a completeness guarantee. Without `--upload` it writes nothing except the file named by `--out`, and it is never invoked by `scan`, `setup`, `guide`, `protect`, or `mark-build`.
|
|
45
|
+
- **`map --upload` is the only command that sends a description of your source.** (The runtime guard can also report rule detections, which carry route paths and parameter names — see "Runtime guard reporting" below.) It POSTs the same JSON document to `monitor/pulse/input-map/<your site uuid>` so Patchstack can pin protection rules to your app's own parameter names instead of guessing them. During a pre-bundle build hook it hashes the policy-relevant document (all fields except analyser timing and memory observations), writes the SHA-256 value as `_patchstack.build_id` in the existing rules file imported by the scaffolded guard, and sends the same value as `build_id`. Outside that lifecycle it sends no identity and changes no file, so any generated scoped rule remains detect-only. **No source code, no file contents, no environment variable values.** A map with no recognised entry points is still uploaded because its import inventory and coverage limits are evidence; a failure to reach Patchstack is reported and ignored rather than failing your build. Omit the flag and the command stays entirely local.
|
|
45
46
|
- **`demo-guide node-serialize` is the read-only companion.** It checks the Host-created site configuration and vulnerable lockfile entry, explains the complete local prepare/run/restart/prove/cleanup sequence, and prints the next exact command. It does not require a deployment and does not change files or contact Patchstack.
|
|
46
47
|
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
47
48
|
|
|
@@ -142,6 +143,46 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
|
|
|
142
143
|
|
|
143
144
|
`setup` performs both steps automatically. The explicit commands are for manual setup or repair. If verification reports a generic or existing framework seam, complete the printed source edit and re-run `--check`; do not report protection as active until it exits successfully.
|
|
144
145
|
|
|
146
|
+
`--check` reads the app's source. It can establish that the guard is imported and called on a request
|
|
147
|
+
path; it cannot establish that a request ever reaches it — an app can wire the guard onto one server
|
|
148
|
+
and serve traffic from another, and that passes. To settle the difference there is an opt-in check
|
|
149
|
+
that **starts the application**:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
npx @patchstack/connect protect --check --runtime
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
It launches the project's entry with `node`, moves the HTTP listeners **that process** opens to an
|
|
156
|
+
ephemeral loopback port, sends one request per listener carrying a per-run challenge, and reports
|
|
157
|
+
whether the scaffolded guard seam answered it. Exit `0` runtime traversal reached the seam, `1` a
|
|
158
|
+
listener answered and the seam did not, `2` it could not be established — neither a pass nor a
|
|
159
|
+
failure, with the structural checks still standing on their own.
|
|
160
|
+
|
|
161
|
+
Exit `2` is the answer for everything this cannot speak for, and the reason is always printed. The
|
|
162
|
+
common one is an entry that needs the project's own toolchain (a TypeScript entry, a framework
|
|
163
|
+
launcher, a watcher, another runtime, anything reached through a package manager), which this never
|
|
164
|
+
installs, builds or invents. The others are about scope: **the run answers for one process, one
|
|
165
|
+
thread, and one discovery window.** If the app attempts to start another process, the launch is
|
|
166
|
+
refused and the answer is `2`. A child can daemonize after it starts without declaring that in its
|
|
167
|
+
launch options, so allowing it would make the end-of-run process-group cleanup a claim the verifier
|
|
168
|
+
cannot establish. The app sees `EPERM`. A worker thread is also `2`: it inherits the listener
|
|
169
|
+
handling, but it cannot report back, so its listeners can be neither counted nor asked. So is a
|
|
170
|
+
listener that bound an address other than loopback, one that cannot be probed, and anything the app
|
|
171
|
+
opens after the discovery window has closed — the app is asked to stop and its acknowledgement is
|
|
172
|
+
what closes that window, so a run that never gets one is `2` as well. An inherited `NODE_OPTIONS`
|
|
173
|
+
that would run code before the listener handling is in place — a `--require` or `--import` in your
|
|
174
|
+
environment — is `2` too, and is refused before the app is launched rather than after.
|
|
175
|
+
|
|
176
|
+
A worker handed a replacement environment that does not preserve the propagated `NODE_OPTIONS` is
|
|
177
|
+
refused outright, because it would not load the listener handling. The app sees `EPERM`, and the run
|
|
178
|
+
reports `2`.
|
|
179
|
+
|
|
180
|
+
What a pass says is exactly: **runtime traversal reached the scaffolded guard seam.** It does not say
|
|
181
|
+
rules were delivered, that the deployed app is wired, or that ordinary traffic is blocked.
|
|
182
|
+
|
|
183
|
+
Nothing else runs the application. `protect`, `protect --check`, `setup`, `guide`, `scan`, `status`
|
|
184
|
+
and `mark-build` only read and write files.
|
|
185
|
+
|
|
145
186
|
5. **Commit** `.patchstackrc.json`, the updated `package.json`, the guard/framework source changes, and the layout/HTML file carrying the widget tag (and the production marker, when `scan` wrote one into a JSX root), so every developer and CI run reports to the same site.
|
|
146
187
|
|
|
147
188
|
**Do not commit `.patchstackrc.local.json`.** That file holds the API key issued at provision; the scan writes it and adds it to `.gitignore`, and tells you if it could not. `.patchstackrc.json` holds only the site UUID and settings, and the UUID is public by design — it ships in the widget tag in served HTML.
|
|
@@ -157,6 +198,50 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
|
|
|
157
198
|
- If a step fails, stop and report it. Don't proceed with placeholders.
|
|
158
199
|
- CI never has the credential in a file: `.patchstackrc.local.json` is git-ignored by design, so set `PATCHSTACK_API_KEY` as an env var there (and `PATCHSTACK_SITE_UUID` too where `.patchstackrc.json` is also absent). Precedence for the site UUID and settings: CLI flag → env var → `.patchstackrc.json`. For the API key: env var → `.patchstackrc.local.json` → `.patchstackrc.json` (where installs made before the split still hold it). `login` is interactive and refuses to run in CI, so CI always takes its credential from the environment.
|
|
159
200
|
|
|
201
|
+
## Which build a rule belongs to
|
|
202
|
+
|
|
203
|
+
Some protection rules are **scoped to your app's own coordinates** — a specific route and a specific
|
|
204
|
+
field name, which Patchstack learns from `map --upload`. A coordinate is only true of the source it was
|
|
205
|
+
read from: rename the field two deploys later and the rule addresses something that no longer exists,
|
|
206
|
+
while still reporting as active protection. Coverage that is not there is worse than a known gap.
|
|
207
|
+
|
|
208
|
+
Such a rule carries a `build_scope` naming the policy map its coordinate came from, and it blocks only
|
|
209
|
+
when Patchstack **confirms** those coordinates belong to the map carried by the guard now running. Three moving
|
|
210
|
+
parts:
|
|
211
|
+
|
|
212
|
+
- **`scan`, during `prebuild`**, removes any previous `_patchstack.build_id` from the guard's own rules
|
|
213
|
+
file before the bundler runs. It never does this from `postinstall`, a manual scan, or `--dry-run`.
|
|
214
|
+
- **`map --upload`, in that same pre-bundle lifecycle**, derives a SHA-256 identity from the map's policy
|
|
215
|
+
content (excluding analyser timing and memory observations),
|
|
216
|
+
writes it into the existing rules file the scaffolded guard imports, and sends the same identifier
|
|
217
|
+
beside the coordinates. It refuses to guess among several guard bundles and never creates one.
|
|
218
|
+
- **The runtime guard** presents it on the rules request it already makes, as `X-Patchstack-Build`, on
|
|
219
|
+
requests that already carry your site credential. Patchstack answers on both full and `304` responses
|
|
220
|
+
with `X-Patchstack-Build-Match`; a match also names the confirmed map in
|
|
221
|
+
`X-Patchstack-Build-ID`. Only that explicit confirmation lets a scoped rule block.
|
|
222
|
+
|
|
223
|
+
**Presenting an identifier is not confirmation.** A missing verdict, an unrecognised one, or one naming
|
|
224
|
+
a different map all leave scoped rules detecting only — which is also what happens against a
|
|
225
|
+
Patchstack that does not implement the verdict yet.
|
|
226
|
+
|
|
227
|
+
**The identifier is the map's content digest, not a repository or platform value.** Two different maps
|
|
228
|
+
cannot be treated as the same merely because they came from the same commit, and a dirty working tree is
|
|
229
|
+
bound to the coordinates actually read. A build that does not run `map --upload` before bundling simply
|
|
230
|
+
has no identifier, which is an ordinary outcome rather than an error.
|
|
231
|
+
|
|
232
|
+
What happens without one: **ordinary rules keep enforcing exactly as they do now**, including versioned
|
|
233
|
+
rules — only scoped rules drop to detect-only, with the reason reported through `onError`. The same
|
|
234
|
+
applies to a cached ruleset fetched for a different map, one cached before this mechanism existed,
|
|
235
|
+
and a ruleset served by the older token-authenticated endpoint, which does not implement the verdict.
|
|
236
|
+
|
|
237
|
+
Rules you pass to `createProtection` yourself follow the same rule and not a looser one: supplying a
|
|
238
|
+
rule locally establishes that you intend it, not that its coordinate still describes the code running.
|
|
239
|
+
A scoped rule you supply blocks when it names the map identity this guard reports (`buildId`), or when you
|
|
240
|
+
set **`trustLocalRuleScope: true`** to take responsibility for the match.
|
|
241
|
+
|
|
242
|
+
Nothing here is sent unless you have already opted into `map --upload`. The identifier is a one-way
|
|
243
|
+
digest of the map — never a message, an author, a diff, a branch name, source text, or environment value.
|
|
244
|
+
|
|
160
245
|
## Runtime guard reporting
|
|
161
246
|
|
|
162
247
|
The runtime guard (`protect`) can report the rules that matched, so the dashboard can show what a rule
|
|
@@ -391,7 +476,8 @@ Two more endpoints the package can call, for completeness:
|
|
|
391
476
|
## Verifying the install
|
|
392
477
|
|
|
393
478
|
- `npx @patchstack/connect status` re-prints the site UUID and dashboard URL, and checks whether the site still exists on Patchstack (`Site status: active / removed / could not be verified`).
|
|
394
|
-
- `npx @patchstack/connect protect --check` verifies the runtime guard is connected to the request path.
|
|
479
|
+
- `npx @patchstack/connect protect --check` verifies from the source that the runtime guard is connected to the request path. It does not run the app.
|
|
480
|
+
- `npx @patchstack/connect protect --check --runtime` additionally **starts the app** on a loopback port and sends it one request, to establish that a request reaches the guard seam. Opt-in, and the only command that runs the application; exit `0`/`1`/`2` as described in step 4.
|
|
395
481
|
- Load the site in a browser — the "Report a vulnerability" button should appear. Refresh a page that was already open before the tag was added: the button only loads with the page.
|
|
396
482
|
- On the deployed site, the button appears only after a deploy that includes these source changes.
|
|
397
483
|
|
|
@@ -404,6 +490,54 @@ These are **two independent states** — never conflate them:
|
|
|
404
490
|
|
|
405
491
|
Local files alone cannot tell you whether the site was removed from Patchstack. Run `npx @patchstack/connect status` and read the `Site status` line, then answer with both states. For example, when the site was removed but the local files remain, say: *"The site itself was removed from Patchstack — reporting has stopped and the widget no longer renders. The local integration code (widget tag, `.patchstackrc.json`, `.patchstackrc.local.json`, the dependency) is still in the project; want me to remove it?"* — not "Patchstack is still installed."
|
|
406
492
|
|
|
493
|
+
## Attaching the site to an account — `claim`
|
|
494
|
+
|
|
495
|
+
A scan provisions the site without an owner. It is monitored from that moment, but its reports are
|
|
496
|
+
only visible once someone attaches it to a Patchstack account. Opening the dashboard link that `scan`
|
|
497
|
+
and `status` print does that. `claim` does the same thing from the terminal, for when the link is
|
|
498
|
+
output nobody is looking at.
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
npx @patchstack/connect claim
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
Your code: BQDX-7ZKM
|
|
506
|
+
Claim at: https://api.patchstack.com/monitor/pulse/device?code=BQDX-7ZKM
|
|
507
|
+
|
|
508
|
+
Open that link and sign in to Patchstack — or create an account — to attach
|
|
509
|
+
this site to it. Whoever approves becomes the site's owner.
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**If you are an assistant running this, the sequence is three steps:**
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
1. npx @patchstack/connect claim → prints the link, exits straight away
|
|
516
|
+
2. Give the user the link. Wait for them to say they have done it.
|
|
517
|
+
3. npx @patchstack/connect claim → the SAME command again, after they confirm.
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
- **Step 1 exits immediately** when the output is piped or captured, rather than blocking for ten
|
|
521
|
+
minutes on a link you cannot see yet.
|
|
522
|
+
- **Step 3 is the same command.** While a request is still valid it resumes rather than restarting, so
|
|
523
|
+
running `claim` again never invalidates the link the user is looking at. If they have not finished
|
|
524
|
+
yet it says so, with the time remaining, and exits 0.
|
|
525
|
+
- **An already-claimed site exits 0, not 1.** It is the goal state. Re-running after the user claimed
|
|
526
|
+
in the browser reports that and stops; it is not a setup failure.
|
|
527
|
+
|
|
528
|
+
`claim --wait` is the blocking variant. Prefer the plain re-run — it keeps each command short, which
|
|
529
|
+
is what fits a conversation.
|
|
530
|
+
|
|
531
|
+
### What it does not do
|
|
532
|
+
|
|
533
|
+
- **It does not rotate the credential.** The project already holds one from provisioning, and CI,
|
|
534
|
+
deploys and other checkouts keep working. (`login` is the command that rotates; use it only to
|
|
535
|
+
recover a lost credential.) A credential is written only when the server issues one for a checkout
|
|
536
|
+
that had none.
|
|
537
|
+
- **It does not open a browser**, and it cannot claim on the user's behalf: the approval is a person
|
|
538
|
+
signing in to Patchstack.
|
|
539
|
+
- **It does not work in CI** — there is no browser and no one to sign in. It refuses and exits 1.
|
|
540
|
+
|
|
407
541
|
## Recovering a lost credential — `login`
|
|
408
542
|
|
|
409
543
|
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`.
|
package/README.md
CHANGED
|
@@ -73,6 +73,13 @@ patchstack-connect protect Install/reconcile the always-
|
|
|
73
73
|
guard. Auto-wires supported server stacks;
|
|
74
74
|
use --check to verify or --demo for local rules.
|
|
75
75
|
Also run by setup; never run by scan/guide/mark-build.
|
|
76
|
+
--check reads your source and never runs the app.
|
|
77
|
+
--check --runtime STARTS THE APP on a loopback
|
|
78
|
+
port and sends it one request, to establish that
|
|
79
|
+
a request reaches the guard seam. Opt-in, and the
|
|
80
|
+
only mode that runs the app. Exit 0 traversed,
|
|
81
|
+
1 a listener answered instead of the guard,
|
|
82
|
+
2 could not be established (see below).
|
|
76
83
|
patchstack-connect map [--dir p] [--out f] [--upload]
|
|
77
84
|
Print a JSON map of this project's attack
|
|
78
85
|
surface: server entry points, the inputs each
|
|
@@ -80,8 +87,9 @@ patchstack-connect map [--dir p] [--out f] [--upload]
|
|
|
80
87
|
system, process, outbound HTTP) and the npm
|
|
81
88
|
package behind each sink. READS YOUR SOURCE
|
|
82
89
|
FILES locally and parses them with the
|
|
83
|
-
project's own TypeScript; writes
|
|
84
|
-
|
|
90
|
+
project's own TypeScript; writes only --out unless
|
|
91
|
+
a pre-bundle --upload stamps the existing guard
|
|
92
|
+
rules file, and posts nothing without --upload. Never run by
|
|
85
93
|
scan/setup/guide/protect — run it yourself.
|
|
86
94
|
patchstack-connect demo node-serialize Production-backed walkthrough: require
|
|
87
95
|
node-serialize@0.0.4, scan it, wait for live
|
|
@@ -90,6 +98,24 @@ patchstack-connect demo node-serialize Production-backed walkthrough
|
|
|
90
98
|
patchstack-connect demo-guide node-serialize Read-only, state-aware instructions for the
|
|
91
99
|
local demo, including the next exact command,
|
|
92
100
|
expected proof, and cleanup.
|
|
101
|
+
patchstack-connect claim [--wait] Attach this site to a Patchstack account from
|
|
102
|
+
the terminal. Prints a link for the user to open
|
|
103
|
+
and sign in (or sign up); whoever approves becomes
|
|
104
|
+
the site's owner. Piped or captured, it prints the
|
|
105
|
+
link and exits — run it again once the user
|
|
106
|
+
confirms. Does not rotate the credential. Same
|
|
107
|
+
result as opening the dashboard link scan prints.
|
|
108
|
+
Not usable in CI
|
|
109
|
+
patchstack-connect login [--wait] Recover this site's credential when
|
|
110
|
+
.patchstackrc.local.json has been lost. Prints a
|
|
111
|
+
link for the site's OWNER to approve. Approving
|
|
112
|
+
ROTATES the credential, so CI, deploys and other
|
|
113
|
+
machines using the old one must be updated.
|
|
114
|
+
Not usable in CI
|
|
115
|
+
patchstack-connect uninstall [options] Signal Patchstack that this package is being
|
|
116
|
+
removed. An unclaimed site record is deleted; a
|
|
117
|
+
claimed one is flagged for its owner. Does NOT
|
|
118
|
+
touch local files
|
|
93
119
|
patchstack-connect help Print help
|
|
94
120
|
patchstack-connect --version Print the installed version
|
|
95
121
|
|
|
@@ -105,6 +131,69 @@ Options (for demo and demo-guide):
|
|
|
105
131
|
(default: http://localhost:3000/api/tasks)
|
|
106
132
|
```
|
|
107
133
|
|
|
134
|
+
### Verifying the guard at runtime (opt-in)
|
|
135
|
+
|
|
136
|
+
`protect --check` reads the app's source. That establishes the guard is imported and called on a
|
|
137
|
+
request path — not that a request ever reaches it. An app can wire the guard onto one server and serve
|
|
138
|
+
its traffic from another, and the structural check passes.
|
|
139
|
+
|
|
140
|
+
`protect --check --runtime` settles that one question by **starting the application**:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
npx @patchstack/connect protect --check --runtime
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It runs the project's entry with `node`, moves the HTTP listeners **that process** opens to an ephemeral
|
|
147
|
+
loopback port (so a port already in use is not a failure), sends one request per listener carrying a
|
|
148
|
+
challenge generated for that run, and reports whether the scaffolded guard seam answered it. The child
|
|
149
|
+
is started in its own process group and killed with it — including if you interrupt the command. A
|
|
150
|
+
listener it cannot probe — a Unix socket, a file descriptor, a handed-over handle, an HTTP/2 server — is
|
|
151
|
+
reported and prevented from binding at all, rather than opened on the verifier's behalf.
|
|
152
|
+
|
|
153
|
+
**One process is the scope — one thread of it, and one discovery window.** If the app attempts to start
|
|
154
|
+
another process, the launch is refused and the answer is `2`, whatever that process is. A child can
|
|
155
|
+
daemonize after it starts without declaring that in its launch options, so allowing it would make the
|
|
156
|
+
end-of-run process-group cleanup a claim the verifier cannot establish. The app sees the launch fail
|
|
157
|
+
with `EPERM`. A **worker thread** is also `2`: it inherits the listener handling, but a worker has no
|
|
158
|
+
channel back, so its listeners can be neither counted nor asked. A worker handed a replacement
|
|
159
|
+
environment that does not preserve the propagated `NODE_OPTIONS` is refused, because it would not load
|
|
160
|
+
that handling at all.
|
|
161
|
+
|
|
162
|
+
Everything else the run finds also ends it this way: a listener that bound an address other than
|
|
163
|
+
loopback, a listener it cannot probe, and anything the app opens **after the discovery window closes** —
|
|
164
|
+
a second listener appearing while the first is still being asked cannot join a set that is already being
|
|
165
|
+
answered from. Closing that window is a handshake: the app is asked to stop opening listeners and its
|
|
166
|
+
acknowledgement is what proves nothing is still in flight, so a run that never gets one reports `2`
|
|
167
|
+
rather than passing. An inherited `NODE_OPTIONS` is checked before anything is launched, too: Node reads
|
|
168
|
+
that variable ahead of the command line, so a `--require` or `--import` sitting in your environment would
|
|
169
|
+
run before the listener handling was in place, and the run reports `2` rather than starting the app with
|
|
170
|
+
less containment than it claims. Recognised flags are passed through, with the reporter first.
|
|
171
|
+
|
|
172
|
+
A pass says exactly this: **runtime traversal reached the scaffolded guard seam.** It does not say
|
|
173
|
+
rules were delivered, that the deployed app is wired, or that ordinary traffic is blocked. The
|
|
174
|
+
challenge is generated per run, so a fixed response or a reflected header cannot answer it — but the
|
|
175
|
+
challenge does reach the whole app process, so this establishes traversal in a cooperating app rather
|
|
176
|
+
than against an app written to answer for itself.
|
|
177
|
+
|
|
178
|
+
| Exit | Meaning |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `0` | A request reached the scaffolded guard seam. |
|
|
181
|
+
| `1` | A listener answered and the seam did not — or the structural checks failed, in which case the app is not started at all. |
|
|
182
|
+
| `2` | It could not be established. Neither a pass nor a failure; the structural checks still stand. |
|
|
183
|
+
|
|
184
|
+
Exit `2` is the common answer for entries this deliberately will not start. It runs `node <file>` on a
|
|
185
|
+
file the project already has, and nothing else — no package-manager scripts, no `node_modules/.bin`, no
|
|
186
|
+
build, no install. So a TypeScript entry, a framework launcher (`next start`), a watcher (`nodemon`),
|
|
187
|
+
another runtime (`bun`), or a script wrapped in an environment shim all report unavailable with the
|
|
188
|
+
reason printed. To make such a project verifiable, point a `start` script at a built, directly loadable
|
|
189
|
+
file — the check reports which entry it used and where it came from.
|
|
190
|
+
|
|
191
|
+
Windows reports exit `2` without starting anything: the cleanup this relies on is a POSIX process
|
|
192
|
+
group, and a verification that can leave a server running is worse than an unanswered question.
|
|
193
|
+
|
|
194
|
+
No other command runs your application. `protect`, `protect --check`, `setup`, `guide`, `scan`,
|
|
195
|
+
`status` and `mark-build` only read and write files.
|
|
196
|
+
|
|
108
197
|
## Configuration
|
|
109
198
|
|
|
110
199
|
Precedence (highest wins):
|
|
@@ -178,6 +267,8 @@ PATCHSTACK_ENVIRONMENT=sandbox npx @patchstack/connect setup
|
|
|
178
267
|
|
|
179
268
|
The generated `prebuild` scan deliberately carries no hard-coded environment. A production builder with no override reports `production`; a preview/sandbox builder must receive `PATCHSTACK_ENVIRONMENT=sandbox` from its host. Runtime protection itself is not environment-specific: `PATCHSTACK_ENVIRONMENT` labels manifests only. Use `PATCHSTACK_MODE=dry-run` when protection should observe rather than block.
|
|
180
269
|
|
|
270
|
+
During a build, the `prebuild` scan removes any previous map stamp. A later `map --upload` in the same pre-bundle lifecycle hashes the map's policy content (excluding analyser timing and memory observations) and records that identity in the guard's existing rules file. The guard presents it on the rules request it already makes; only an explicit Patchstack confirmation naming the same map lets a rule scoped to one of your app's parameter names block. A build with no confirmed identity still enforces every ordinary rule — only scoped rules drop to detect-only, with the reason reported. Outside a pre-bundle hook, `map --upload` changes no file and sends no identity. See "Which build a rule belongs to" in `AGENT-INSTALL.md`.
|
|
271
|
+
|
|
181
272
|
### `scan` as a build hook
|
|
182
273
|
|
|
183
274
|
`setup` wires `scan` into `postinstall`, `prebuild`, or the Bun `build` chain. Run from one of those, a report Patchstack cannot accept — no credential in the build environment, a rejected credential, a site that no longer exists, an outage — is printed on stderr and `scan` exits 0, so the install or build it is attached to carries on. Patchstack keeps the last manifest it accepted for the site until a scan that can report. Run directly (`npx @patchstack/connect scan`), the same failure exits 1.
|
|
@@ -272,7 +363,7 @@ Why you might want it: the two `lodash` entries above are not a contrived exampl
|
|
|
272
363
|
|
|
273
364
|
These are repo-relative locations built from `node_modules` segments, plus a workspace directory name when a workspace pins its own copy. They come from the lockfile's own keys or from the `node_modules` walk — **never from your source tree**. No path to a file you wrote is sent by either form of `scan`.
|
|
274
365
|
|
|
275
|
-
`installPathsComplete` says whether the set is total. It is `false` without the flag, and `false` with it whenever the source cannot supply locations — a `yarn.lock` is flat because hoisting is decided at install time, and a v1 `package-lock.json` records the dependency graph rather than the installed tree. Whenever it is `false`, a missing `paths` means **"not recorded"**, never "not installed there". (The `map` command reads source files locally to report your attack surface; it transmits nothing unless you pass `--upload`, which sends that structural description — route paths, parameter names, the dependency behind each sink, and file/line locations, never file contents — to your own site's endpoint so rules can be pinned to your real parameter names.) Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed. (`mark-build` separately stamps built HTML with a stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL` — never their values.)
|
|
366
|
+
`installPathsComplete` says whether the set is total. It is `false` without the flag, and `false` with it whenever the source cannot supply locations — a `yarn.lock` is flat because hoisting is decided at install time, and a v1 `package-lock.json` records the dependency graph rather than the installed tree. Whenever it is `false`, a missing `paths` means **"not recorded"**, never "not installed there". (The `map` command reads source files locally to report your attack surface; it transmits nothing unless you pass `--upload`, which sends that structural description — route paths, parameter names, the dependency behind each sink, and file/line locations, never file contents — to your own site's endpoint so rules can be pinned to your real parameter names. During a pre-bundle hook it also sends a digest of that exact map and records the same value in the guard's rules file; see "Which build a rule belongs to" in `AGENT-INSTALL.md`.) Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed. (`mark-build` separately stamps built HTML with a stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL` — never their values.)
|
|
276
367
|
|
|
277
368
|
## Supported lockfiles
|
|
278
369
|
|
|
@@ -80,8 +80,25 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
80
80
|
return first.response;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// src/build-id.ts
|
|
84
|
+
var BUILD_ID = /^[0-9a-f]{64}$/i;
|
|
85
|
+
function canonicalBuildId(value) {
|
|
86
|
+
if (typeof value !== "string") return null;
|
|
87
|
+
const trimmed = value.trim();
|
|
88
|
+
return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
|
|
89
|
+
}
|
|
90
|
+
var BUILD_STAMP_KEY = "_patchstack";
|
|
91
|
+
function readBuildStamp(bundle) {
|
|
92
|
+
if (bundle === null || typeof bundle !== "object") return null;
|
|
93
|
+
const namespace = bundle[BUILD_STAMP_KEY];
|
|
94
|
+
if (namespace === null || typeof namespace !== "object") return null;
|
|
95
|
+
return canonicalBuildId(namespace.build_id);
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
export {
|
|
84
99
|
pulseAuthHeader,
|
|
85
|
-
pulseFetch
|
|
100
|
+
pulseFetch,
|
|
101
|
+
canonicalBuildId,
|
|
102
|
+
readBuildStamp
|
|
86
103
|
};
|
|
87
|
-
//# sourceMappingURL=chunk-
|
|
104
|
+
//# sourceMappingURL=chunk-IOODP4ZB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pulse-token.ts","../src/build-id.ts"],"mappings":";AAUA,IAAM,gBAAgB;AAGf,SAAS,cAAc,kBAAkC;AAC9D,QAAM,MAAM,IAAI,IAAI,gBAAgB;AACpC,QAAM,OAAO,IAAI,SAAS,QAAQ,OAAO,EAAE;AAC3C,MAAI,WAAW,KAAK,SAAS,WAAW,IACpC,GAAG,KAAK,MAAM,GAAG,CAAC,YAAY,MAAM,CAAC,WACrC;AACJ,MAAI,SAAS;AACb,MAAI,OAAO;AACX,SAAO,IAAI,SAAS;AACtB;AAGO,SAAS,eAAe,YAAuE;AACpG,QAAM,QAAQ,WAAW,YAAY,GAAG;AACxC,MAAI,SAAS,KAAK,UAAU,WAAW,SAAS,EAAG,QAAO;AAE1D,QAAM,WAAW,WAAW,MAAM,QAAQ,CAAC;AAC3C,MAAI,CAAC,QAAQ,KAAK,QAAQ,EAAG,QAAO;AAEpC,SAAO,EAAE,UAAU,cAAc,WAAW,MAAM,GAAG,KAAK,EAAE;AAC9D;AAEA,IAAI,SAAsD;AAC1D,IAAI,WAA0C;AAGvC,SAAS,kBAAwB;AACtC,WAAS;AACX;AAWA,eAAsB,cACpB,QACA,YAA0B,OACF;AAGxB,MAAI,OAAO,OAAO,cAAc,YAAY,OAAO,UAAU,WAAW,EAAG,QAAO;AAElF,MAAI,WAAW,QAAQ,KAAK,IAAI,IAAI,OAAO,YAAY,eAAe;AACpE,WAAO,OAAO;AAAA,EAChB;AACA,MAAI,aAAa,KAAM,QAAO;AAE9B,QAAM,cAAc,eAAe,OAAO,SAAS;AACnD,MAAI,gBAAgB,KAAM,QAAO;AAEjC,cAAY,YAAY;AACtB,QAAI;AACF,YAAM,WAAW,MAAM,UAAU,cAAc,OAAO,QAAQ,GAAG;AAAA,QAC/D,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,QAAQ;AAAA,UACR,cAAc;AAAA,QAChB;AAAA,QACA,MAAM,KAAK,UAAU;AAAA,UACnB,YAAY;AAAA,UACZ,WAAW,YAAY;AAAA,UACvB,eAAe,YAAY;AAAA,QAC7B,CAAC;AAAA,QACD,QAAQ,YAAY,QAAQ,OAAO,SAAS;AAAA,MAC9C,CAAC;AAED,UAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,YAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,UAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,WAAW,EAAG,QAAO;AAEpF,YAAM,YAAY,OAAO,KAAK,UAAU;AACxC,YAAM,QAAQ,OAAO,SAAS,SAAS,KAAK,YAAY,IAAI,YAAY,MAAO;AAC/E,eAAS,EAAE,OAAO,KAAK,cAAc,WAAW,KAAK,IAAI,IAAI,MAAM;AAEnE,aAAO,KAAK;AAAA,IACd,QAAQ;AACN,aAAO;AAAA,IACT,UAAE;AACA,iBAAW;AAAA,IACb;AAAA,EACF,GAAG;AAEH,SAAO;AACT;AAMA,eAAsB,gBACpB,QACA,YAA0B,OACO;AACjC,QAAM,QAAQ,MAAM,cAAc,QAAQ,SAAS;AACnD,SAAO,UAAU,OAAO,CAAC,IAAI,EAAE,eAAe,UAAU,KAAK,GAAG;AAClE;AAcA,eAAsB,WACpB,QACA,KACA,MACA,YAA0B,OACP;AACnB,QAAM,OAAO,YAAY;AACvB,UAAM,OAAO,MAAM,gBAAgB,QAAQ,SAAS;AACpD,UAAM,WAAW,MAAM,UAAU,KAAK;AAAA,MACpC,GAAG;AAAA,MACH,SAAS,EAAE,GAAI,KAAK,SAAgD,GAAG,KAAK;AAAA,IAC9E,CAAC;AAED,WAAO,EAAE,UAAU,eAAe,KAAK,kBAAkB,OAAU;AAAA,EACrE;AAEA,QAAM,QAAQ,MAAM,KAAK;AAIzB,MAAI,MAAM,SAAS,WAAW,OAAO,MAAM,eAAe;AACxD,oBAAgB;AAEhB,YAAQ,MAAM,KAAK,GAAG;AAAA,EACxB;AAEA,SAAO,MAAM;AACf;;;AC3IA,IAAM,WAAW;AAWV,SAAS,iBAAiB,OAA+B;AAC9D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAE3B,SAAO,SAAS,KAAK,OAAO,IAAI,QAAQ,YAAY,IAAI;AAC1D;AASO,IAAM,kBAAkB;AASxB,SAAS,eAAe,QAAgC;AAC7D,MAAI,WAAW,QAAQ,OAAO,WAAW,SAAU,QAAO;AAC1D,QAAM,YAAa,OAAmC,eAAe;AACrE,MAAI,cAAc,QAAQ,OAAO,cAAc,SAAU,QAAO;AAEhE,SAAO,iBAAkB,UAAsC,QAAQ;AACzE;","names":[]}
|