Skip to content

Commit e33e1e4

Browse files
committed
fix(protect): per-piece idempotent start.ts wiring (clean upgrades) — 0.3.2
The single-marker idempotency check (skip if patchstackGuard present) meant re-running protect after a connect upgrade silently did nothing — so an app wired by 0.3.0 never gained the new server-function guard. Rework patchStart to reconcile each piece independently: extend the guard import, add each middleware definition, and register each guard only when absent. Re-running now adds exactly what's missing (verified: 0.3.0-wired app gains patchstackFunctionGuard with no duplication; a second run is a no-op).
1 parent c2274a2 commit e33e1e4

2 files changed

Lines changed: 47 additions & 15 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@patchstack/connect",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"description": "Patchstack connector for JavaScript applications. Scans your lockfile and reports installed packages to Patchstack for vulnerability monitoring.",
55
"keywords": [
66
"patchstack",

src/protect/install.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const START_IMPORTS = [
3939
'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";',
4040
].join('\n');
4141

42-
const START_MIDDLEWARE = [
42+
const REQUEST_MIDDLEWARE_DEF = [
4343
'',
4444
'// Patchstack guard (browser tunnel): intercept tunneled Supabase traffic before anything else.',
4545
'const patchstackGuard = createMiddleware().server(async ({ next }) => {',
@@ -51,6 +51,10 @@ const START_MIDDLEWARE = [
5151
' return next();',
5252
'});',
5353
'',
54+
].join('\n');
55+
56+
const FUNCTION_MIDDLEWARE_DEF = [
57+
'',
5458
'// Patchstack guard (server functions): inspect server-fn args before they reach the database,',
5559
'// covering apps that mutate via TanStack server functions (which bypass the browser tunnel).',
5660
'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
@@ -94,25 +98,53 @@ function patchClient(cwd: string): void {
9498
function patchStart(cwd: string): void {
9599
const p = join(cwd, 'src/start.ts');
96100
let s = read(p);
97-
if (s.includes('patchstackGuard')) return log('start.ts already wired');
98101
const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
99102
const exportAnchor = 'export const startInstance';
100103
const rmAnchor = 'requestMiddleware: [';
101-
if (!s.includes(importAnchor) || !s.includes(exportAnchor) || !s.includes(rmAnchor)) {
104+
if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
102105
return log('start.ts anchors not found — skipping (template changed?)');
103106
}
104-
s = s.replace(importAnchor, importAnchor + '\n' + START_IMPORTS);
105-
s = s.replace(exportAnchor, START_MIDDLEWARE + '\n' + exportAnchor);
106-
// Browser-tunnel guard → request middleware (covers browser-direct Supabase apps).
107-
s = s.replace(rmAnchor, rmAnchor + 'patchstackGuard, ');
108-
// Server-function guard → function middleware (covers apps that mutate via server functions).
109-
const fmAnchor = 'functionMiddleware: [';
110-
if (s.includes(fmAnchor)) {
111-
s = s.replace(fmAnchor, fmAnchor + 'patchstackFunctionGuard, ');
112-
} else {
113-
// App declared no functionMiddleware — add the key next to requestMiddleware.
114-
s = s.replace(rmAnchor, 'functionMiddleware: [patchstackFunctionGuard],\n ' + rmAnchor);
107+
108+
// Each step is independently idempotent, so re-running `protect` (including after a connect
109+
// upgrade that adds a new guard) reconciles only what's missing — never duplicates, never
110+
// silently skips a newly-added piece.
111+
const original = s;
112+
113+
// Imports.
114+
if (!s.includes('@/integrations/patchstack/guard')) {
115+
s = s.replace(importAnchor, importAnchor + '\n' + START_IMPORTS);
116+
} else if (!s.includes('inspectServerFn')) {
117+
// Upgrade from a build that only wired the browser tunnel: pull in inspectServerFn.
118+
s = s.replace(
119+
'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";',
120+
'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";',
121+
);
122+
}
123+
124+
// Middleware definitions (each only if its const isn't already present).
125+
if (!s.includes('const patchstackGuard =')) {
126+
s = s.replace(exportAnchor, REQUEST_MIDDLEWARE_DEF + '\n' + exportAnchor);
115127
}
128+
if (!s.includes('const patchstackFunctionGuard =')) {
129+
s = s.replace(exportAnchor, FUNCTION_MIDDLEWARE_DEF + '\n' + exportAnchor);
130+
}
131+
132+
// Register the browser-tunnel guard in requestMiddleware.
133+
if (s.includes(rmAnchor) && !s.includes('requestMiddleware: [patchstackGuard')) {
134+
s = s.replace(rmAnchor, rmAnchor + 'patchstackGuard, ');
135+
}
136+
137+
// Register the server-function guard in functionMiddleware (create the key if the app has none).
138+
if (!s.includes('functionMiddleware: [patchstackFunctionGuard')) {
139+
const fmAnchor = 'functionMiddleware: [';
140+
if (s.includes(fmAnchor)) {
141+
s = s.replace(fmAnchor, fmAnchor + 'patchstackFunctionGuard, ');
142+
} else if (s.includes(rmAnchor)) {
143+
s = s.replace(rmAnchor, 'functionMiddleware: [patchstackFunctionGuard],\n ' + rmAnchor);
144+
}
145+
}
146+
147+
if (s === original) return log('start.ts already wired');
116148
writeFileSync(p, s);
117149
log('patched start.ts (guard registered as request + function middleware)');
118150
}

0 commit comments

Comments
 (0)