@patchstack/connect 0.5.0 → 0.5.2
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 +95 -2
- package/README.md +105 -1
- package/dist/cli.js +987 -145
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +9 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +9 -2
- 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 +43 -5
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +14 -0
- package/dist/protect.d.ts +14 -0
- package/dist/protect.edge.js +30 -1
- package/dist/protect.edge.js.map +2 -2
- package/dist/protect.js +31 -2
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-LDS35UKV.js → refresh-manifest-2MMPQN2A.js} +10 -3
- package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-LDS35UKV.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Patchstack runtime protection — GENERIC guard (CommonJS). Managed by `patchstack-connect protect`.
|
|
2
2
|
// Wire whichever helper fits your server into your request path, then run `protect --check`.
|
|
3
|
-
const { createProtection } = require("@patchstack/connect/protect");
|
|
3
|
+
const { createProtection, sentinelAnswer, VERIFY_HEADER } = require("@patchstack/connect/protect");
|
|
4
4
|
// The fallback bundle is optional at RUNTIME. This file is imported on the app's own module path, so a
|
|
5
5
|
// throw here is the app failing to boot rather than protection failing open — and a rules file can be
|
|
6
6
|
// absent for ordinary reasons: a bundler that copied no JSON, a partial deploy, a half-written edit.
|
|
@@ -66,6 +66,12 @@ function psStepAside(err) {
|
|
|
66
66
|
}
|
|
67
67
|
function protectFetch(handler) {
|
|
68
68
|
return async (request, ...rest) => {
|
|
69
|
+
// `protect --check --runtime` asks whether a request actually reaches this seam. The answer is
|
|
70
|
+
// derived from a challenge the verifying process mints per run, which rules out an app matching it
|
|
71
|
+
// by accident, and there is nothing to answer unless that process started this one. Without a
|
|
72
|
+
// challenge in the environment this is a header read.
|
|
73
|
+
const answered = await sentinelAnswer(request.headers.get(VERIFY_HEADER));
|
|
74
|
+
if (answered) return new Response(answered, { status: 200, headers: { "content-type": "text/plain" } });
|
|
69
75
|
const active = await getProtection().catch(psStepAside);
|
|
70
76
|
// The handler, and nothing around it: an exception the app throws is the app's, not a protection
|
|
71
77
|
// failure, and must not be read as one or answered twice.
|
|
@@ -92,19 +98,37 @@ function patchstackMiddleware(req, res, next) {
|
|
|
92
98
|
passedOn = true;
|
|
93
99
|
next(err);
|
|
94
100
|
};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
// `protect --check --runtime` asks whether a request actually reaches this seam. Answered here,
|
|
102
|
+
// before the protection is asked for and before the request is passed on, so no handler of the app's
|
|
103
|
+
// ever sees a verification request. The answer is derived from a challenge the verifying process mints
|
|
104
|
+
// per run, which rules out an app matching it by accident; without a challenge in the environment
|
|
105
|
+
// there is nothing to answer and the request is screened as normal.
|
|
106
|
+
const screen = () => {
|
|
107
|
+
getProtection().then(
|
|
108
|
+
(active) => active.node()(req, res, carryOn),
|
|
109
|
+
(err) => {
|
|
110
|
+
psStepAside(err);
|
|
111
|
+
carryOn();
|
|
112
|
+
},
|
|
113
|
+
).catch((err) => {
|
|
114
|
+
// Not a failed build: the guard, or the app's own chain, threw after this point. Reported, and the
|
|
115
|
+
// request is carried on only if it never was — an error here must not take the process down and
|
|
116
|
+
// must not answer twice.
|
|
98
117
|
psStepAside(err);
|
|
99
118
|
carryOn();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
sentinelAnswer(req.headers?.[VERIFY_HEADER]).then((answered) => {
|
|
123
|
+
if (answered) {
|
|
124
|
+
res.statusCode = 200;
|
|
125
|
+
res.setHeader("content-type", "text/plain");
|
|
126
|
+
res.end(answered);
|
|
127
|
+
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
screen();
|
|
131
|
+
}, screen);
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
module.exports = { getProtection, protectFetch, patchstackMiddleware };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Patchstack runtime protection — GENERIC guard (ESM). Managed by `patchstack-connect protect`.
|
|
2
2
|
// Wire whichever helper fits your server into your request path, then run `protect --check`.
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { createProtection } from "@patchstack/connect/protect";
|
|
4
|
+
import { createProtection, sentinelAnswer, VERIFY_HEADER } from "@patchstack/connect/protect";
|
|
5
5
|
|
|
6
6
|
// The fallback bundle is optional at RUNTIME. This file is imported on the app's own module path, so a
|
|
7
7
|
// throw here is the app failing to boot rather than protection failing open — and a rules file can be
|
|
@@ -68,6 +68,12 @@ function psStepAside(err) {
|
|
|
68
68
|
// Web-Fetch: export default { fetch: protectFetch(originalFetch) }
|
|
69
69
|
export function protectFetch(handler) {
|
|
70
70
|
return async (request, ...rest) => {
|
|
71
|
+
// `protect --check --runtime` asks whether a request actually reaches this seam. The answer is
|
|
72
|
+
// derived from a challenge the verifying process mints per run, which rules out an app matching it
|
|
73
|
+
// by accident, and there is nothing to answer unless that process started this one. Without a
|
|
74
|
+
// challenge in the environment this is a header read.
|
|
75
|
+
const answered = await sentinelAnswer(request.headers.get(VERIFY_HEADER));
|
|
76
|
+
if (answered) return new Response(answered, { status: 200, headers: { "content-type": "text/plain" } });
|
|
71
77
|
const active = await getProtection().catch(psStepAside);
|
|
72
78
|
// The handler, and nothing around it: an exception the app throws is the app's, not a protection
|
|
73
79
|
// failure, and must not be read as one or answered twice.
|
|
@@ -94,17 +100,35 @@ export function patchstackMiddleware(req, res, next) {
|
|
|
94
100
|
passedOn = true;
|
|
95
101
|
next(err);
|
|
96
102
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
// `protect --check --runtime` asks whether a request actually reaches this seam. Answered here,
|
|
104
|
+
// before the protection is asked for and before the request is passed on, so no handler of the app's
|
|
105
|
+
// ever sees a verification request. The answer is derived from a challenge the verifying process mints
|
|
106
|
+
// per run, which rules out an app matching it by accident; without a challenge in the environment
|
|
107
|
+
// there is nothing to answer and the request is screened as normal.
|
|
108
|
+
const screen = () => {
|
|
109
|
+
getProtection().then(
|
|
110
|
+
(active) => active.node()(req, res, carryOn),
|
|
111
|
+
(err) => {
|
|
112
|
+
psStepAside(err);
|
|
113
|
+
carryOn();
|
|
114
|
+
},
|
|
115
|
+
).catch((err) => {
|
|
116
|
+
// Not a failed build: the guard, or the app's own chain, threw after this point. Reported, and the
|
|
117
|
+
// request is carried on only if it never was — an error here must not take the process down and
|
|
118
|
+
// must not answer twice.
|
|
100
119
|
psStepAside(err);
|
|
101
120
|
carryOn();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
sentinelAnswer(req.headers?.[VERIFY_HEADER]).then((answered) => {
|
|
125
|
+
if (answered) {
|
|
126
|
+
res.statusCode = 200;
|
|
127
|
+
res.setHeader("content-type", "text/plain");
|
|
128
|
+
res.end(answered);
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
screen();
|
|
133
|
+
}, screen);
|
|
110
134
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// whichever helper below fits your server into your request path (see the plan the CLI printed),
|
|
5
5
|
// then run `patchstack-connect protect --check` to confirm it's hooked up. The engine ships inside
|
|
6
6
|
// @patchstack/connect — nothing else to install.
|
|
7
|
-
import { createProtection } from "@patchstack/connect/protect";
|
|
7
|
+
import { createProtection, sentinelAnswer, VERIFY_HEADER } from "@patchstack/connect/protect";
|
|
8
8
|
import fallbackRules from "./rules.json";
|
|
9
9
|
|
|
10
10
|
// Baked by `patchstack-connect protect` from .patchstackrc.json when available.
|
|
@@ -76,6 +76,12 @@ function psStepAside(err: unknown) {
|
|
|
76
76
|
// Wrap your fetch handler: export default { fetch: protectFetch(originalFetch) }
|
|
77
77
|
export function protectFetch<H extends (request: Request, ...rest: unknown[]) => unknown>(handler: H): H {
|
|
78
78
|
return (async (request: Request, ...rest: unknown[]) => {
|
|
79
|
+
// `protect --check --runtime` asks whether a request actually reaches this seam. The answer is
|
|
80
|
+
// derived from a challenge the verifying process mints per run, which rules out an app matching it
|
|
81
|
+
// by accident, and there is nothing to answer unless that process started this one. Without a
|
|
82
|
+
// challenge in the environment this is a header read.
|
|
83
|
+
const answered = await sentinelAnswer(request.headers.get(VERIFY_HEADER));
|
|
84
|
+
if (answered) return new Response(answered, { status: 200, headers: { "content-type": "text/plain" } });
|
|
79
85
|
const protection = await getProtection().catch(psStepAside);
|
|
80
86
|
// The handler, and nothing around it: an exception the app throws is the app's, not a protection
|
|
81
87
|
// failure, and must not be read as one or answered twice.
|
|
@@ -103,17 +109,40 @@ export function patchstackMiddleware(req: unknown, res: unknown, next: (err?: un
|
|
|
103
109
|
passedOn = true;
|
|
104
110
|
next(err);
|
|
105
111
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
// `protect --check --runtime` asks whether a request actually reaches this seam. Answered here,
|
|
113
|
+
// before the protection is asked for and before the request is passed on, so no handler of the app's
|
|
114
|
+
// ever sees a verification request. The answer is derived from a challenge the verifying process mints
|
|
115
|
+
// per run, which rules out an app matching it by accident; without a challenge in the environment
|
|
116
|
+
// there is nothing to answer and the request is screened as normal.
|
|
117
|
+
const screen = () => {
|
|
118
|
+
getProtection().then(
|
|
119
|
+
(protection) => (protection.node() as (a: unknown, b: unknown, c: (e?: unknown) => void) => void)(req, res, carryOn),
|
|
120
|
+
(err) => {
|
|
121
|
+
psStepAside(err);
|
|
122
|
+
carryOn();
|
|
123
|
+
},
|
|
124
|
+
).catch((err) => {
|
|
125
|
+
// Not a failed build: the guard, or the app's own chain, threw after this point. Reported, and the
|
|
126
|
+
// request is carried on only if it never was — an error here must not take the process down and
|
|
127
|
+
// must not answer twice.
|
|
109
128
|
psStepAside(err);
|
|
110
129
|
carryOn();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// The two shapes this seam touches, named rather than asserted wholesale: a request whose headers it
|
|
134
|
+
// reads, and a response it answers on.
|
|
135
|
+
const inbound = req as { headers?: Record<string, unknown> };
|
|
136
|
+
const outbound = res as { statusCode: number; setHeader(name: string, value: string): void; end(body?: string): void };
|
|
137
|
+
|
|
138
|
+
sentinelAnswer(inbound.headers?.[VERIFY_HEADER]).then((answered) => {
|
|
139
|
+
if (answered) {
|
|
140
|
+
outbound.statusCode = 200;
|
|
141
|
+
outbound.setHeader("content-type", "text/plain");
|
|
142
|
+
outbound.end(answered);
|
|
143
|
+
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
screen();
|
|
147
|
+
}, screen);
|
|
119
148
|
}
|
package/dist/protect.cjs
CHANGED
|
@@ -142,6 +142,9 @@ var init_types = __esm({
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
// src/client.ts
|
|
145
|
+
function claimTokenHeader(config) {
|
|
146
|
+
return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
147
|
+
}
|
|
145
148
|
function buildEndpointUrl(base, siteUuid) {
|
|
146
149
|
const trimmed = base.replace(/\/$/, "");
|
|
147
150
|
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
@@ -177,7 +180,8 @@ async function postManifest(config, payload) {
|
|
|
177
180
|
headers: {
|
|
178
181
|
"Content-Type": "application/json",
|
|
179
182
|
Accept: "application/json",
|
|
180
|
-
"User-Agent": "@patchstack/connect"
|
|
183
|
+
"User-Agent": "@patchstack/connect",
|
|
184
|
+
...claimTokenHeader(config)
|
|
181
185
|
},
|
|
182
186
|
body: JSON.stringify(buildManifestBody(config, payload)),
|
|
183
187
|
signal: AbortSignal.timeout(timeoutMs)
|
|
@@ -236,7 +240,7 @@ function isTimeoutError(cause) {
|
|
|
236
240
|
}
|
|
237
241
|
return false;
|
|
238
242
|
}
|
|
239
|
-
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS;
|
|
243
|
+
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS, CLAIM_TOKEN_HEADER;
|
|
240
244
|
var init_client = __esm({
|
|
241
245
|
"src/client.ts"() {
|
|
242
246
|
"use strict";
|
|
@@ -244,6 +248,7 @@ var init_client = __esm({
|
|
|
244
248
|
init_pulse_token();
|
|
245
249
|
DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
246
250
|
DEFAULT_TIMEOUT_MS = 3e4;
|
|
251
|
+
CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
247
252
|
}
|
|
248
253
|
});
|
|
249
254
|
|
|
@@ -478,6 +483,7 @@ async function resolveConfig(options) {
|
|
|
478
483
|
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
479
484
|
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
480
485
|
const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
|
|
486
|
+
const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
|
|
481
487
|
return {
|
|
482
488
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
483
489
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
@@ -487,7 +493,8 @@ async function resolveConfig(options) {
|
|
|
487
493
|
endpoint,
|
|
488
494
|
timeoutMs,
|
|
489
495
|
environment,
|
|
490
|
-
widget: fromFile.widget !== false
|
|
496
|
+
widget: fromFile.widget !== false,
|
|
497
|
+
claimToken
|
|
491
498
|
};
|
|
492
499
|
}
|
|
493
500
|
async function readSecretFile(cwd) {
|
|
@@ -1838,9 +1845,11 @@ var runtime_exports = {};
|
|
|
1838
1845
|
__export(runtime_exports, {
|
|
1839
1846
|
GUARD_PATH: () => GUARD_PATH,
|
|
1840
1847
|
PulseRuleClient: () => PulseRuleClient,
|
|
1848
|
+
VERIFY_HEADER: () => VERIFY_HEADER,
|
|
1841
1849
|
createProtection: () => createProtection,
|
|
1842
1850
|
createServerFnGuard: () => createServerFnGuard,
|
|
1843
|
-
createSupabaseGuard: () => createSupabaseGuard
|
|
1851
|
+
createSupabaseGuard: () => createSupabaseGuard,
|
|
1852
|
+
sentinelAnswer: () => sentinelAnswer
|
|
1844
1853
|
});
|
|
1845
1854
|
module.exports = __toCommonJS(runtime_exports);
|
|
1846
1855
|
|
|
@@ -6323,6 +6332,33 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
6323
6332
|
};
|
|
6324
6333
|
}
|
|
6325
6334
|
|
|
6335
|
+
// src/protect/verify-sentinel.js
|
|
6336
|
+
var VERIFY_HEADER = "x-patchstack-verify";
|
|
6337
|
+
var CHALLENGE = "PATCHSTACK_VERIFY_CHALLENGE";
|
|
6338
|
+
function configuredChallenge() {
|
|
6339
|
+
const value = typeof process !== "undefined" ? process.env?.[CHALLENGE] : void 0;
|
|
6340
|
+
return typeof value === "string" && value.length >= 32 ? value : null;
|
|
6341
|
+
}
|
|
6342
|
+
function sameValue(a, b) {
|
|
6343
|
+
if (typeof a !== "string" || typeof b !== "string" || a.length !== b.length) return false;
|
|
6344
|
+
let differs = 0;
|
|
6345
|
+
for (let i = 0; i < a.length; i += 1) differs |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
6346
|
+
return differs === 0;
|
|
6347
|
+
}
|
|
6348
|
+
async function sentinelAnswer(offered) {
|
|
6349
|
+
const challenge = configuredChallenge();
|
|
6350
|
+
if (challenge === null || !sameValue(challenge, typeof offered === "string" ? offered : null)) return null;
|
|
6351
|
+
const subtle = globalThis.crypto?.subtle;
|
|
6352
|
+
if (!subtle || typeof subtle.digest !== "function" || typeof TextEncoder === "undefined") return null;
|
|
6353
|
+
try {
|
|
6354
|
+
const bytes = new TextEncoder().encode(`patchstack-verify:${challenge}`);
|
|
6355
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
6356
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6357
|
+
} catch {
|
|
6358
|
+
return null;
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6326
6362
|
// src/protect/runtime.js
|
|
6327
6363
|
function createServerFnGuard({ protection }) {
|
|
6328
6364
|
const guard = protection.fetchGuard();
|
|
@@ -7660,8 +7696,10 @@ function nodeRequestMeta(req, client) {
|
|
|
7660
7696
|
0 && (module.exports = {
|
|
7661
7697
|
GUARD_PATH,
|
|
7662
7698
|
PulseRuleClient,
|
|
7699
|
+
VERIFY_HEADER,
|
|
7663
7700
|
createProtection,
|
|
7664
7701
|
createServerFnGuard,
|
|
7665
|
-
createSupabaseGuard
|
|
7702
|
+
createSupabaseGuard,
|
|
7703
|
+
sentinelAnswer
|
|
7666
7704
|
});
|
|
7667
7705
|
//# sourceMappingURL=protect.cjs.map
|