@patchstack/connect 0.5.1 → 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 +91 -1
- package/README.md +88 -0
- package/dist/cli.js +950 -145
- package/dist/cli.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 +33 -2
- 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 +30 -1
- package/dist/protect.js.map +1 -1
- package/package.json +1 -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
|
@@ -1845,9 +1845,11 @@ var runtime_exports = {};
|
|
|
1845
1845
|
__export(runtime_exports, {
|
|
1846
1846
|
GUARD_PATH: () => GUARD_PATH,
|
|
1847
1847
|
PulseRuleClient: () => PulseRuleClient,
|
|
1848
|
+
VERIFY_HEADER: () => VERIFY_HEADER,
|
|
1848
1849
|
createProtection: () => createProtection,
|
|
1849
1850
|
createServerFnGuard: () => createServerFnGuard,
|
|
1850
|
-
createSupabaseGuard: () => createSupabaseGuard
|
|
1851
|
+
createSupabaseGuard: () => createSupabaseGuard,
|
|
1852
|
+
sentinelAnswer: () => sentinelAnswer
|
|
1851
1853
|
});
|
|
1852
1854
|
module.exports = __toCommonJS(runtime_exports);
|
|
1853
1855
|
|
|
@@ -6330,6 +6332,33 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
6330
6332
|
};
|
|
6331
6333
|
}
|
|
6332
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
|
+
|
|
6333
6362
|
// src/protect/runtime.js
|
|
6334
6363
|
function createServerFnGuard({ protection }) {
|
|
6335
6364
|
const guard = protection.fetchGuard();
|
|
@@ -7667,8 +7696,10 @@ function nodeRequestMeta(req, client) {
|
|
|
7667
7696
|
0 && (module.exports = {
|
|
7668
7697
|
GUARD_PATH,
|
|
7669
7698
|
PulseRuleClient,
|
|
7699
|
+
VERIFY_HEADER,
|
|
7670
7700
|
createProtection,
|
|
7671
7701
|
createServerFnGuard,
|
|
7672
|
-
createSupabaseGuard
|
|
7702
|
+
createSupabaseGuard,
|
|
7703
|
+
sentinelAnswer
|
|
7673
7704
|
});
|
|
7674
7705
|
//# sourceMappingURL=protect.cjs.map
|