@patchstack/connect 0.5.6 → 0.5.8

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 CHANGED
@@ -88,7 +88,7 @@ This is a request, not a mechanism: nothing in the install depends on it. Do it
88
88
 
89
89
  **If the request you were given includes a claim token**, pass it exactly as given: `npx @patchstack/connect setup --claim-token <token>`. The site is then created in the person's own Patchstack account and `setup` prints their dashboard link for it — there is no sign-in step to relay. The token comes only from the person's Patchstack dashboard; never invent one, never write it to a file, never print it back. If `setup` reports that the token had expired or was not recognised, the site is not connected: hand over the dashboard link it prints instead, and tell the person they can copy a fresh prompt from the dashboard.
90
90
 
91
- In a hosted builder, run setup with `PATCHSTACK_ENVIRONMENT=sandbox` scoped to the workspace process/command, ensure the CLI's on-disk edits are adopted into the platform's persisted project state, then restart any already-running preview/server process so it loads the guard. Do not persist `"environment": "sandbox"` in `.patchstackrc.json`: deployed builds use the same committed files and report `production` only when the platform's own production signal says so (Vercel, Netlify, Render, Railway); a preview there reports `sandbox`. A build in a project the builder generated and builds for itself (Lovable, Replit) reports `production` without an override, because the edit preview is a dev server and a build is the publish step — which is exactly why the sandbox label belongs in the workspace process and not in a file. A scan on a developer's machine, in a generic CI runner, or on a platform with no such signal reports `local` on its own, and the dashboard shows that app as configured, not deployed, until its build is seen live. A client-only SPA or a static site generator has no server request path to guard: `setup` says runtime protection does not apply and installs nothing for it; never call such a project protected.
91
+ In a hosted builder, run setup with `PATCHSTACK_ENVIRONMENT=sandbox` scoped to the workspace process/command, ensure the CLI's on-disk edits are adopted into the platform's persisted project state, then restart any already-running preview/server process so it loads the guard. Do not persist `"environment": "sandbox"` in `.patchstackrc.json`: deployed builds use the same committed files and report `production` only when the build platform's own variables say so — its tier (Vercel, Netlify, Render, Railway, GitLab CI) or a branch named `main`, `master`, `production`, `prod`, `release` or `live` (Cloudflare Pages and Workers Builds, AWS Amplify, GitHub Actions, GitLab CI without a tier); a preview, pull request or other branch there reports `sandbox`, as does the Replit workspace, while a Replit Deployment reports `production`. A build in a project the builder generated and builds for itself (Lovable, Replit) reports `production` without an override, because the edit preview is a dev server and a build is the publish step — which is exactly why the sandbox label belongs in the workspace process and not in a file. A scan on a developer's machine, in a CI runner this does not know (`CI=true` alone), or on a platform with no such signal reports `local` on its own, and the dashboard shows that app as configured, not deployed. A client-only SPA or a static site generator has no server request path to guard: `setup` says runtime protection does not apply and installs nothing for it; never call such a project protected.
92
92
 
93
93
  **Finish by telling the user to refresh their preview.** The widget's "Report a vulnerability" button loads with the page, so a preview that was already open still shows the HTML from before setup — the button is missing there until it reloads. Nothing in the CLI can reach the user's browser, so relaying this is your job. Phrase it as a check rather than a required step: a builder that hot reloads, or a preview server you restarted, may have refreshed it already.
94
94
 
@@ -194,7 +194,7 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
194
194
  - Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
195
195
  - Never invent or guess a claim token either. One is only ever handed to you by the person, from their own Patchstack dashboard; pass it with `--claim-token` (or `PATCHSTACK_CLAIM_TOKEN`) and nowhere else — not into `.patchstackrc.json`, not into a committed file, not into your reply.
196
196
  - The CLI never opens the dashboard link and never asks for Patchstack credentials.
197
- - Label hosted workspace scans with `PATCHSTACK_ENVIRONMENT=sandbox` in that process only. Leave production builds unset (a platform's own production signal, or the hosted builder the project belongs to, makes the build report `production`; a developer machine or a generic CI runner reports `local`) and never commit a sandbox label into files shared with production.
197
+ - Label hosted workspace scans with `PATCHSTACK_ENVIRONMENT=sandbox` in that process only. Leave production builds unset (a platform's own tier or production branch name, or the hosted builder the project belongs to, makes the build report `production`; a developer machine or a CI runner this does not know reports `local`) and never commit a sandbox label into files shared with production.
198
198
  - If a step fails, stop and report it. Don't proceed with placeholders.
199
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.
200
200
 
package/README.md CHANGED
@@ -208,7 +208,7 @@ Environment variables:
208
208
  - `PATCHSTACK_SITE_UUID` — the site UUID from your Patchstack dashboard
209
209
  - `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://api.patchstack.com/monitor/pulse/manifest`)
210
210
  - `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`)
211
- - `PATCHSTACK_ENVIRONMENT` — manifest label: `production`, `sandbox` or `local`. Unset, the label comes from the hosting platform's own production/preview signal (Vercel, Netlify, Render, Railway); a preview reports `sandbox`. Failing that, a project a hosted builder generated and builds for itself (Lovable, Replit) reports `production`, because on those platforms the edit preview is a dev server and a build only happens when the owner publishes. Anything else — a developer machine, a generic CI runner, a platform this does not know — reports `local`
211
+ - `PATCHSTACK_ENVIRONMENT` — manifest label: `production`, `sandbox` or `local`. Unset, the label comes from the build platform's own variables. Where the platform names the tier (Vercel, Netlify, Render, Railway, GitLab CI), production reports `production` and a preview `sandbox`. Where it names only the branch (Cloudflare Pages and Workers Builds, AWS Amplify, GitHub Actions, GitLab CI without a tier), a branch named `main`, `master`, `production`, `prod`, `release` or `live` reports `production` and any other branch or a pull request `sandbox`. A Replit Deployment reports `production`, the Replit workspace `sandbox`. Failing all of that, a project a hosted builder generated and builds for itself (Lovable, Replit) reports `production`, because on those platforms the edit preview is a dev server and a build only happens when the owner publishes. Anything else — a developer machine, a CI runner this does not know (`CI=true` alone), a platform this does not know — reports `local`
212
212
  - `PATCHSTACK_CLAIM_TOKEN` — connect the site straight to your account (see *Connecting straight to your account*)
213
213
 
214
214
  Two files, because one value is public and the other is not.
@@ -257,7 +257,7 @@ The token names your account, not the project: it is never written to `.patchsta
257
257
 
258
258
  ### Sandbox and production manifests
259
259
 
260
- Every `scan` sends an environment label with its dependency manifest. When nothing sets one, the label comes from the hosting platform's own answer to "is this the production deployment?": Vercel's `VERCEL_ENV`, Netlify's `CONTEXT`, Render's pull-request flag, Railway's environment name. Production reports `production`; a preview those platforms name as such reports `sandbox`. Where no platform answers, the project itself gets the last word: one a hosted builder generated and builds for itself — Lovable, Replit — reports `production`, because those platforms set no variable to read and their edit preview is a dev server, so a build running at all is the owner publishing. Everything else reports `local` — a developer's machine, a generic CI runner (`CI=true` proves automation, not deployment), and a platform whose build environment carries no such signal, Cloudflare Pages among them. A local manifest is inventory — it tells Patchstack what the app is built from — and never counts as contact with a live site, so an app that has only been set up on a laptop shows in the dashboard as **Configured locally**, not as connected or deployed; once its build is seen on the live site it reads as deployed regardless of the label. Set `PATCHSTACK_ENVIRONMENT=production` where builds run on a platform this list does not know. Sandboxed builders should set `PATCHSTACK_ENVIRONMENT=sandbox` in the sandbox process only. Patchstack stores and deduplicates manifests per environment, so an iterative workspace scan does not replace the last production manifest.
260
+ Every `scan` sends an environment label with its dependency manifest. When nothing sets one, the label comes from the build platform's own variables. Platforms that name the tier answer directly: Vercel's `VERCEL_ENV`, Netlify's `CONTEXT`, Render's pull-request flag, Railway's environment name, GitLab's `CI_ENVIRONMENT_TIER`. Production reports `production`; a preview those platforms name as such reports `sandbox`. Platforms that name only the branch — Cloudflare Pages and Workers Builds, AWS Amplify, GitHub Actions, GitLab CI without a tier — are decided by the branch name: `main`, `master`, `production`, `prod`, `release` and `live` report `production`, any other branch reports `sandbox`, and a pull-request build reports `sandbox` whatever the branch. That is an assumption about naming, so the line `scan` prints says the decision rests on the branch name; set `PATCHSTACK_ENVIRONMENT` where the live branch is called something else. A Replit Deployment reports `production` and the Replit workspace `sandbox`. Where no platform answers, the project itself gets the last word: one a hosted builder generated and builds for itself — Lovable, Replit — reports `production`, because the edit preview there is a dev server, so a build running at all is the owner publishing. Everything else reports `local` — a developer's machine, a CI runner this does not know (`CI=true` alone proves automation, not deployment), and a platform whose build environment carries no such signal. A local manifest is inventory — it tells Patchstack what the app is built from — and never counts as contact with a live site, so an app that has only been set up on a laptop shows in the dashboard as **Configured locally**, not as connected or deployed. The label also decides whether `mark-build` stamps the live-site marker, so a deployment that reads `local` ships its pages without one: set `PATCHSTACK_ENVIRONMENT=production` where builds run on a platform this list does not know. Sandboxed builders should set `PATCHSTACK_ENVIRONMENT=sandbox` in the sandbox process only. Patchstack stores and deduplicates manifests per environment, so an iterative workspace scan does not replace the last production manifest.
261
261
 
262
262
  Do not commit `"environment": "sandbox"` to `.patchstackrc.json` when the same files are deployed to production. Scope the variable to the sandbox command/process instead:
263
263
 
@@ -265,7 +265,7 @@ Do not commit `"environment": "sandbox"` to `.patchstackrc.json` when the same f
265
265
  PATCHSTACK_ENVIRONMENT=sandbox npx @patchstack/connect setup
266
266
  ```
267
267
 
268
- The generated `prebuild` scan deliberately carries no hard-coded environment. A production build with no override reports `production` only because the platform's own production signal says it is one; a preview on those platforms reports `sandbox` by itself, and a hosted builder's *workspace* must receive `PATCHSTACK_ENVIRONMENT=sandbox` from its host — its published build needs no override, because a build in such a project is the publish step. Runtime protection itself is not environment-specific: `PATCHSTACK_ENVIRONMENT` labels manifests only. Use `PATCHSTACK_MODE=dry-run` when protection should observe rather than block.
268
+ The generated `prebuild` scan deliberately carries no hard-coded environment. A production build with no override reports `production` only because the platform's own variables — its tier, or its branch name — say it is one; a preview or pull request on those platforms reports `sandbox` by itself, and a hosted builder's *workspace* must receive `PATCHSTACK_ENVIRONMENT=sandbox` from its host (the Replit workspace reports `sandbox` on its own) — its published build needs no override, because a build in such a project is the publish step. Runtime protection itself is not environment-specific: `PATCHSTACK_ENVIRONMENT` labels manifests only. Use `PATCHSTACK_MODE=dry-run` when protection should observe rather than block.
269
269
 
270
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
271
 
@@ -311,7 +311,7 @@ The widget is a floating "Report a vulnerability" button — a disclosure channe
311
311
 
312
312
  Re-runs update the tag in place (the `data-patchstack-connect-widget` attribute marks it as connector-managed); a pre-existing manual widget tag is left untouched. `--dry-run` never edits anything; a failed post still skips the widget tag (it needs the site UUID) but the production marker may already have been written, since it runs before the post. Projects whose root layout is code rather than HTML (Next.js, Nuxt, Astro, …) get the exact snippet and target file printed instead — `guide` shows framework-specific placement.
313
313
 
314
- - **`mark-build`** ensures the same tag in built HTML output, covering builds whose source shell the connector couldn't edit, and stamps `window.__PATCHSTACK_PROD__` so the widget hides the claim/login UI on the published site (owners reach it by appending `#patchstack` to the live URL). The marker says the page is the live site, so **only a production build carries it**: the environment is read the same way `scan` reads it (the hosting platform's own production discriminator, then the hosted builder the project belongs to), and a local or preview build gets the widget tag, no marker, and any marker an earlier build left behind removed. Publishing a static build by hand from your machine is the case that needs `--production` (or `PATCHSTACK_ENVIRONMENT=production`), because nothing in that environment can say the build is a deployment.
314
+ - **`mark-build`** ensures the same tag in built HTML output, covering builds whose source shell the connector couldn't edit, and stamps `window.__PATCHSTACK_PROD__` so the widget hides the claim/login UI on the published site (owners reach it by appending `#patchstack` to the live URL). The marker says the page is the live site, so **only a production build carries it**: the environment is read the same way `scan` reads it (the build platform's own tier or branch name, then the hosted builder the project belongs to), and a local or preview build gets the widget tag, no marker, and any marker an earlier build left behind removed. Publishing a static build by hand from your machine is the case that needs `--production` (or `PATCHSTACK_ENVIRONMENT=production`), because nothing in that environment can say the build is a deployment.
315
315
 
316
316
  - **Opting out:** persist `"widget": false` in `.patchstackrc.json` to disable both the widget tag and the production marker (dependency scanning only). Without it, the next successful scan re-adds the managed tag, and the next scan re-adds the marker on a JSX root.
317
317
 
@@ -0,0 +1,295 @@
1
+ // src/protect/safe-origin.js
2
+ function isSafeOrigin(value) {
3
+ try {
4
+ const u = new URL(value);
5
+ if (u.protocol === "https:") return true;
6
+ return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]" || u.hostname === "::1");
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+ function safeBaseUrl(candidate, fallback, label) {
12
+ if (typeof candidate !== "string" || candidate === "") return fallback;
13
+ if (isSafeOrigin(candidate)) return candidate;
14
+ warnOnce(label, `[patchstack] ignoring unsafe ${label} override (${candidate}): must be https (or localhost). Using the default.`);
15
+ return fallback;
16
+ }
17
+ var warned = /* @__PURE__ */ new Set();
18
+ function warnOnce(key, message) {
19
+ if (warned.has(key)) return;
20
+ warned.add(key);
21
+ console.warn(message);
22
+ }
23
+
24
+ // src/types.ts
25
+ var PatchstackError = class extends Error {
26
+ constructor(message, code, cause) {
27
+ super(message);
28
+ this.code = code;
29
+ this.cause = cause;
30
+ this.name = "PatchstackError";
31
+ }
32
+ code;
33
+ cause;
34
+ /**
35
+ * For a `VALIDATION_ERROR`, the request fields the server refused, as it named them. Read from the
36
+ * structured `errors` object a validation response carries, so a caller deciding what to do about a
37
+ * refusal keys on the field rather than on the wording of a sentence.
38
+ */
39
+ fields = [];
40
+ };
41
+
42
+ // src/endpoint-policy.ts
43
+ function assertConnectableEndpoint(config, target = config.endpoint) {
44
+ if (config.endpointTrusted === false) {
45
+ throw new PatchstackError(
46
+ "The custom endpoint in .patchstackrc.json must be confirmed with --endpoint or PATCHSTACK_ENDPOINT before it can be used.",
47
+ "CONFIG_INVALID"
48
+ );
49
+ }
50
+ if (safeRemoteUrl(config.endpoint) === null) {
51
+ throw new PatchstackError(
52
+ "Patchstack endpoints must use HTTPS, except for localhost development endpoints.",
53
+ "CONFIG_INVALID"
54
+ );
55
+ }
56
+ let endpointOrigin;
57
+ let targetOrigin;
58
+ try {
59
+ endpointOrigin = new URL(config.endpoint).origin;
60
+ targetOrigin = new URL(target).origin;
61
+ } catch (cause) {
62
+ throw new PatchstackError("The configured Patchstack endpoint is not a valid URL.", "CONFIG_INVALID", cause);
63
+ }
64
+ if (safeRemoteUrl(target) === null || targetOrigin !== endpointOrigin) {
65
+ throw new PatchstackError(
66
+ "A Patchstack request resolved outside the configured endpoint origin.",
67
+ "CONFIG_INVALID"
68
+ );
69
+ }
70
+ }
71
+ function safeRemoteUrl(value) {
72
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048) return null;
73
+ try {
74
+ const parsed = new URL(value);
75
+ if (!isSafeOrigin(parsed.toString()) || parsed.username !== "" || parsed.password !== "") return null;
76
+ return parsed.toString();
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ function safeDisplayString(value, maxLength = 512) {
82
+ if (typeof value !== "string") return null;
83
+ const clean = value.replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
84
+ if (clean.length === 0) return null;
85
+ return clean.slice(0, maxLength);
86
+ }
87
+ function isCanonicalUuid(value) {
88
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
89
+ }
90
+ function isCredential(value) {
91
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096) return false;
92
+ for (let i = 0; i < value.length; i += 1) {
93
+ const code = value.charCodeAt(i);
94
+ if (code < 33 || code > 126) return false;
95
+ }
96
+ const separator = value.lastIndexOf("-");
97
+ return separator > 0 && separator < value.length - 1 && /^\d+$/.test(value.slice(separator + 1));
98
+ }
99
+ function isHeaderValue(value, maxLength = 8192) {
100
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) return false;
101
+ for (let i = 0; i < value.length; i += 1) {
102
+ const code = value.charCodeAt(i);
103
+ if (code < 33 || code > 126) return false;
104
+ }
105
+ return true;
106
+ }
107
+
108
+ // src/bounded-response.ts
109
+ var MAX_CONTROL_RESPONSE_BYTES = 256 * 1024;
110
+ async function readBoundedText(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
111
+ const stated = Number(response.headers?.get?.("content-length"));
112
+ if (Number.isFinite(stated) && stated > maxBytes) {
113
+ void response.body?.cancel().catch(() => {
114
+ });
115
+ throw new Error(`response exceeds ${maxBytes} bytes`);
116
+ }
117
+ const stream = response.body;
118
+ if (stream === null || typeof stream.getReader !== "function") {
119
+ if (stated === 0) return "";
120
+ throw new Error("response body cannot be read with a size bound");
121
+ }
122
+ const reader = stream.getReader();
123
+ const chunks = [];
124
+ let total = 0;
125
+ try {
126
+ for (; ; ) {
127
+ const { done, value } = await reader.read();
128
+ if (done) break;
129
+ total += value.byteLength;
130
+ if (total > maxBytes) {
131
+ void reader.cancel().catch(() => {
132
+ });
133
+ throw new Error(`response exceeds ${maxBytes} bytes`);
134
+ }
135
+ chunks.push(value);
136
+ }
137
+ } finally {
138
+ reader.releaseLock();
139
+ }
140
+ const bytes = new Uint8Array(total);
141
+ let offset = 0;
142
+ for (const chunk of chunks) {
143
+ bytes.set(chunk, offset);
144
+ offset += chunk.byteLength;
145
+ }
146
+ return new TextDecoder().decode(bytes);
147
+ }
148
+ async function readBoundedJson(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
149
+ const text = await readBoundedText(response, maxBytes);
150
+ return JSON.parse(text);
151
+ }
152
+
153
+ // src/pulse-token.ts
154
+ var TOKEN_SKEW_MS = 6e4;
155
+ function buildTokenUrl(manifestEndpoint) {
156
+ const url = new URL(manifestEndpoint);
157
+ const path = url.pathname.replace(/\/$/, "");
158
+ url.pathname = path.endsWith("/manifest") ? `${path.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
159
+ url.search = "";
160
+ url.hash = "";
161
+ return url.toString();
162
+ }
163
+ function parsePulseAuth(credential) {
164
+ const index = credential.lastIndexOf("-");
165
+ if (index <= 0 || index === credential.length - 1) return null;
166
+ const clientId = credential.slice(index + 1);
167
+ if (!/^\d+$/.test(clientId)) return null;
168
+ return { clientId, clientSecret: credential.slice(0, index) };
169
+ }
170
+ var cached = /* @__PURE__ */ new Map();
171
+ var inflight = /* @__PURE__ */ new Map();
172
+ function cacheKey(tokenUrl, credential) {
173
+ return `${tokenUrl}
174
+ ${credential}`;
175
+ }
176
+ function clearPulseToken(config) {
177
+ if (!config || typeof config.pulseAuth !== "string") {
178
+ cached.clear();
179
+ inflight.clear();
180
+ return;
181
+ }
182
+ try {
183
+ const key = cacheKey(buildTokenUrl(config.endpoint), config.pulseAuth);
184
+ cached.delete(key);
185
+ inflight.delete(key);
186
+ } catch {
187
+ }
188
+ }
189
+ async function getPulseToken(config, fetchImpl = fetch) {
190
+ if (typeof config.pulseAuth !== "string" || config.pulseAuth.length === 0) return null;
191
+ if (config.endpointTrusted === false) return null;
192
+ let tokenUrl;
193
+ try {
194
+ tokenUrl = buildTokenUrl(config.endpoint);
195
+ } catch {
196
+ return null;
197
+ }
198
+ if (!isSafeOrigin(tokenUrl)) return null;
199
+ const key = cacheKey(tokenUrl, config.pulseAuth);
200
+ const existing = cached.get(key);
201
+ if (existing !== void 0 && Date.now() < existing.expiresAt - TOKEN_SKEW_MS) {
202
+ return existing.token;
203
+ }
204
+ const pending = inflight.get(key);
205
+ if (pending !== void 0) return pending;
206
+ const credentials = parsePulseAuth(config.pulseAuth);
207
+ if (credentials === null) return null;
208
+ let exchange;
209
+ exchange = (async () => {
210
+ try {
211
+ const response = await fetchImpl(tokenUrl, {
212
+ method: "POST",
213
+ headers: {
214
+ "Content-Type": "application/json",
215
+ Accept: "application/json",
216
+ "User-Agent": "@patchstack/connect"
217
+ },
218
+ body: JSON.stringify({
219
+ grant_type: "client_credentials",
220
+ client_id: credentials.clientId,
221
+ client_secret: credentials.clientSecret
222
+ }),
223
+ signal: AbortSignal.timeout(config.timeoutMs)
224
+ });
225
+ if (!response.ok) return null;
226
+ const body = await readBoundedJson(response);
227
+ if (!isHeaderValue(body.access_token)) return null;
228
+ const expiresIn = Number(body.expires_in);
229
+ const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
230
+ cached.set(key, { token: body.access_token, expiresAt: Date.now() + ttlMs });
231
+ return body.access_token;
232
+ } catch {
233
+ return null;
234
+ } finally {
235
+ if (inflight.get(key) === exchange) inflight.delete(key);
236
+ }
237
+ })();
238
+ inflight.set(key, exchange);
239
+ return exchange;
240
+ }
241
+ async function pulseAuthHeader(config, fetchImpl = fetch) {
242
+ const token = await getPulseToken(config, fetchImpl);
243
+ return token === null ? {} : { Authorization: `Bearer ${token}` };
244
+ }
245
+ async function pulseFetch(config, url, init, fetchImpl = fetch) {
246
+ assertConnectableEndpoint(config, url);
247
+ const send = async () => {
248
+ const auth = await pulseAuthHeader(config, fetchImpl);
249
+ const response = await fetchImpl(url, {
250
+ ...init,
251
+ headers: { ...init.headers, ...auth }
252
+ });
253
+ return { response, authenticated: auth.Authorization !== void 0 };
254
+ };
255
+ const first = await send();
256
+ if (first.response.status === 401 && first.authenticated) {
257
+ clearPulseToken(config);
258
+ return (await send()).response;
259
+ }
260
+ return first.response;
261
+ }
262
+
263
+ // src/build-id.ts
264
+ var BUILD_ID = /^[0-9a-f]{64}$/i;
265
+ function canonicalBuildId(value) {
266
+ if (typeof value !== "string") return null;
267
+ const trimmed = value.trim();
268
+ return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
269
+ }
270
+ var BUILD_STAMP_KEY = "_patchstack";
271
+ function readBuildStamp(bundle) {
272
+ if (bundle === null || typeof bundle !== "object") return null;
273
+ const namespace = bundle[BUILD_STAMP_KEY];
274
+ if (namespace === null || typeof namespace !== "object") return null;
275
+ return canonicalBuildId(namespace.build_id);
276
+ }
277
+
278
+ export {
279
+ isSafeOrigin,
280
+ safeBaseUrl,
281
+ PatchstackError,
282
+ assertConnectableEndpoint,
283
+ safeRemoteUrl,
284
+ safeDisplayString,
285
+ isCanonicalUuid,
286
+ isCredential,
287
+ isHeaderValue,
288
+ readBoundedText,
289
+ readBoundedJson,
290
+ pulseAuthHeader,
291
+ pulseFetch,
292
+ canonicalBuildId,
293
+ readBuildStamp
294
+ };
295
+ //# sourceMappingURL=chunk-4OHAYSV2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/protect/safe-origin.js","../src/types.ts","../src/endpoint-policy.ts","../src/bounded-response.ts","../src/pulse-token.ts","../src/build-id.ts"],"mappings":";AAKO,SAAS,aAAa,OAAO;AAClC,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,KAAK;AACvB,QAAI,EAAE,aAAa,SAAU,QAAO;AACpC,WAAO,EAAE,aAAa,YAAY,EAAE,aAAa,eAAe,EAAE,aAAa,eAAe,EAAE,aAAa,WAAW,EAAE,aAAa;AAAA,EACzI,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,YAAY,WAAW,UAAU,OAAO;AACtD,MAAI,OAAO,cAAc,YAAY,cAAc,GAAI,QAAO;AAC9D,MAAI,aAAa,SAAS,EAAG,QAAO;AACpC,WAAS,OAAO,gCAAgC,KAAK,cAAc,SAAS,qDAAqD;AACjI,SAAO;AACT;AAEA,IAAM,SAAS,oBAAI,IAAI;AACvB,SAAS,SAAS,KAAK,SAAS;AAC9B,MAAI,OAAO,IAAI,GAAG,EAAG;AACrB,SAAO,IAAI,GAAG;AAEd,UAAQ,KAAK,OAAO;AACtB;;;ACiGO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAQzC,YACE,SACgB,MAYA,OAChB;AACA,UAAM,OAAO;AAdG;AAYA;AAGhB,SAAK,OAAO;AAAA,EACd;AAAA,EAhBkB;AAAA,EAYA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAhBX,SAA4B,CAAC;AAqBtC;;;ACxJO,SAAS,0BAA0B,QAAgB,SAAS,OAAO,UAAgB;AACxF,MAAI,OAAO,oBAAoB,OAAO;AACpC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,MAAI,cAAc,OAAO,QAAQ,MAAM,MAAM;AAC3C,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,qBAAiB,IAAI,IAAI,OAAO,QAAQ,EAAE;AAC1C,mBAAe,IAAI,IAAI,MAAM,EAAE;AAAA,EACjC,SAAS,OAAO;AACd,UAAM,IAAI,gBAAgB,0DAA0D,kBAAkB,KAAK;AAAA,EAC7G;AACA,MAAI,cAAc,MAAM,MAAM,QAAQ,iBAAiB,gBAAgB;AACrE,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,cAAc,OAA+B;AAC3D,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,KAAK,MAAM,SAAS,KAAO,QAAO;AACpF,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,QAAI,CAAC,aAAa,OAAO,SAAS,CAAC,KAAK,OAAO,aAAa,MAAM,OAAO,aAAa,GAAI,QAAO;AACjG,WAAO,OAAO,SAAS;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,kBAAkB,OAAgB,YAAY,KAAoB;AAChF,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,QAAQ,MACX,QAAQ,kEAAkE,EAAE,EAC5E,QAAQ,iCAAiC,GAAG,EAC5C,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACR,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO,MAAM,MAAM,GAAG,SAAS;AACjC;AAEO,SAAS,gBAAgB,OAAiC;AAC/D,SACE,OAAO,UAAU,YACjB,kEAAkE,KAAK,KAAK;AAEhF;AAEO,SAAS,aAAa,OAAiC;AAC5D,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,KAAK,MAAM,SAAS,KAAO,QAAO;AACpF,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,UAAM,OAAO,MAAM,WAAW,CAAC;AAC/B,QAAI,OAAO,MAAQ,OAAO,IAAM,QAAO;AAAA,EACzC;AACA,QAAM,YAAY,MAAM,YAAY,GAAG;AACvC,SAAO,YAAY,KAAK,YAAY,MAAM,SAAS,KAAK,QAAQ,KAAK,MAAM,MAAM,YAAY,CAAC,CAAC;AACjG;AAEO,SAAS,cAAc,OAAgB,YAAY,MAAwB;AAChF,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,KAAK,MAAM,SAAS,UAAW,QAAO;AACxF,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,UAAM,OAAO,MAAM,WAAW,CAAC;AAC/B,QAAI,OAAO,MAAQ,OAAO,IAAM,QAAO;AAAA,EACzC;AACA,SAAO;AACT;;;AClFO,IAAM,6BAA6B,MAAM;AAGhD,eAAsB,gBACpB,UACA,WAAW,4BACM;AACjB,QAAM,SAAS,OAAO,SAAS,SAAS,MAAM,gBAAgB,CAAC;AAC/D,MAAI,OAAO,SAAS,MAAM,KAAK,SAAS,UAAU;AAChD,SAAK,SAAS,MAAM,OAAO,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAC3C,UAAM,IAAI,MAAM,oBAAoB,QAAQ,QAAQ;AAAA,EACtD;AAEA,QAAM,SAAS,SAAS;AACxB,MAAI,WAAW,QAAQ,OAAO,OAAO,cAAc,YAAY;AAC7D,QAAI,WAAW,EAAG,QAAO;AACzB,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AAEA,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,SAAuB,CAAC;AAC9B,MAAI,QAAQ;AACZ,MAAI;AACF,eAAS;AACP,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,eAAS,MAAM;AACf,UAAI,QAAQ,UAAU;AACpB,aAAK,OAAO,OAAO,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AACnC,cAAM,IAAI,MAAM,oBAAoB,QAAQ,QAAQ;AAAA,MACtD;AACA,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AAEA,QAAM,QAAQ,IAAI,WAAW,KAAK;AAClC,MAAI,SAAS;AACb,aAAW,SAAS,QAAQ;AAC1B,UAAM,IAAI,OAAO,MAAM;AACvB,cAAU,MAAM;AAAA,EAClB;AACA,SAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AACvC;AAEA,eAAsB,gBACpB,UACA,WAAW,4BACO;AAClB,QAAM,OAAO,MAAM,gBAAgB,UAAU,QAAQ;AACrD,SAAO,KAAK,MAAM,IAAI;AACxB;;;ACvCA,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,IAAM,SAAS,oBAAI,IAAkD;AACrE,IAAM,WAAW,oBAAI,IAAoC;AAEzD,SAAS,SAAS,UAAkB,YAA4B;AAC9D,SAAO,GAAG,QAAQ;AAAA,EAAK,UAAU;AACnC;AAGO,SAAS,gBAAgB,QAAuB;AACrD,MAAI,CAAC,UAAU,OAAO,OAAO,cAAc,UAAU;AACnD,WAAO,MAAM;AACb,aAAS,MAAM;AACf;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,SAAS,cAAc,OAAO,QAAQ,GAAG,OAAO,SAAS;AACrE,WAAO,OAAO,GAAG;AACjB,aAAS,OAAO,GAAG;AAAA,EACrB,QAAQ;AAAA,EAER;AACF;AAWA,eAAsB,cACpB,QACA,YAA0B,OACF;AAGxB,MAAI,OAAO,OAAO,cAAc,YAAY,OAAO,UAAU,WAAW,EAAG,QAAO;AAClF,MAAI,OAAO,oBAAoB,MAAO,QAAO;AAE7C,MAAI;AACJ,MAAI;AACF,eAAW,cAAc,OAAO,QAAQ;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,aAAa,QAAQ,EAAG,QAAO;AAEpC,QAAM,MAAM,SAAS,UAAU,OAAO,SAAS;AAC/C,QAAM,WAAW,OAAO,IAAI,GAAG;AAE/B,MAAI,aAAa,UAAa,KAAK,IAAI,IAAI,SAAS,YAAY,eAAe;AAC7E,WAAO,SAAS;AAAA,EAClB;AACA,QAAM,UAAU,SAAS,IAAI,GAAG;AAChC,MAAI,YAAY,OAAW,QAAO;AAElC,QAAM,cAAc,eAAe,OAAO,SAAS;AACnD,MAAI,gBAAgB,KAAM,QAAO;AAEjC,MAAI;AACJ,cAAY,YAAY;AACtB,QAAI;AACF,YAAM,WAAW,MAAM,UAAU,UAAU;AAAA,QACzC,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,gBAAgB,QAAQ;AAC5C,UAAI,CAAC,cAAc,KAAK,YAAY,EAAG,QAAO;AAE9C,YAAM,YAAY,OAAO,KAAK,UAAU;AACxC,YAAM,QAAQ,OAAO,SAAS,SAAS,KAAK,YAAY,IAAI,YAAY,MAAO;AAC/E,aAAO,IAAI,KAAK,EAAE,OAAO,KAAK,cAAc,WAAW,KAAK,IAAI,IAAI,MAAM,CAAC;AAE3E,aAAO,KAAK;AAAA,IACd,QAAQ;AACN,aAAO;AAAA,IACT,UAAE;AACA,UAAI,SAAS,IAAI,GAAG,MAAM,SAAU,UAAS,OAAO,GAAG;AAAA,IACzD;AAAA,EACF,GAAG;AACH,WAAS,IAAI,KAAK,QAAQ;AAE1B,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,4BAA0B,QAAQ,GAAG;AAErC,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,MAAM;AAEtB,YAAQ,MAAM,KAAK,GAAG;AAAA,EACxB;AAEA,SAAO,MAAM;AACf;;;AC/KA,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":[]}