Skip to content

Commit 43579eb

Browse files
authored
fix: bake site UUID into generated guards (#86)
1 parent b1b8b7f commit 43579eb

11 files changed

Lines changed: 158 additions & 45 deletions

File tree

‎src/protect/install/adapters/next.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
77
import { join } from 'node:path';
8-
import { read, log, templatesDir } from '../util.js';
8+
import { bakeSiteUuid, read, log, templatesDir } from '../util.js';
99
import type { Adapter, WireOptions, WireResult, VerifyResult } from '../types.js';
1010

1111
function hasNextDep(cwd: string): boolean {
@@ -64,6 +64,7 @@ function wire(cwd: string, opts: WireOptions): WireResult {
6464

6565
// Fresh (or already-ours) → write the managed middleware.
6666
copyFileSync(join(templates, 'next-middleware.ts'), mwPath);
67+
if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
6768
changed.push(mw.relFile);
6869
log(mw.exists ? `refreshed ${mw.relFile} (Patchstack middleware)` : `scaffolded ${mw.relFile} (Patchstack middleware)`);
6970
return { ok: true, changed: [...new Set(changed)] };

‎src/protect/install/adapters/tanstack-supabase.ts‎

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
99
import { join } from 'node:path';
10-
import { read, log, templatesDir } from '../util.js';
10+
import { bakeSiteUuid, read, log, templatesDir } from '../util.js';
1111
import type { Adapter, WireOptions, WireResult, VerifyResult } from '../types.js';
1212

1313
const CLIENT_TUNNEL = [
@@ -133,40 +133,6 @@ function scaffold(cwd: string, opts: WireOptions): string[] {
133133
return changed;
134134
}
135135

136-
// Bake the site UUID from .patchstackrc.json (written by `scan`) into the scaffolded guard, so the
137-
// deployed Worker calls the live Pulse rules API with zero user config. Left as the inert
138-
// placeholder when the app hasn't been scanned yet or the file can't be read. Returns whether it baked.
139-
function bakeSiteUuid(cwd: string): boolean {
140-
const rc = join(cwd, '.patchstackrc.json');
141-
if (!existsSync(rc)) {
142-
log('no .patchstackrc.json — guard uses PATCHSTACK_SITE_UUID env or the bundled fallback');
143-
return false;
144-
}
145-
let uuid: string | undefined;
146-
try {
147-
uuid = JSON.parse(read(rc)).siteUuid;
148-
} catch {
149-
log('.patchstackrc.json unreadable — skipping site-UUID bake');
150-
return false;
151-
}
152-
// Guard on UUID format so a malformed value falls through to the inert placeholder rather than
153-
// baking junk into a TS string literal (broken build / replace-token hazards).
154-
if (!uuid || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
155-
log('.patchstackrc.json siteUuid missing or malformed — guard uses PATCHSTACK_SITE_UUID env or the bundled fallback');
156-
return false;
157-
}
158-
const p = join(cwd, GUARD_FILE);
159-
if (!existsSync(p)) return false;
160-
const s = read(p);
161-
if (!s.includes('__PATCHSTACK_SITE_UUID__')) {
162-
log('guard.ts site UUID already baked');
163-
return false;
164-
}
165-
writeFileSync(p, s.replace('__PATCHSTACK_SITE_UUID__', uuid));
166-
log('baked site UUID into guard.ts — live rules from the Patchstack API');
167-
return true;
168-
}
169-
170136
function patchClient(cwd: string): boolean {
171137
const p = join(cwd, 'src/integrations/supabase/client.ts');
172138
const s = read(p);
@@ -238,7 +204,7 @@ function wire(cwd: string, opts: WireOptions): WireResult {
238204
const changed = scaffold(cwd, opts);
239205
// In demo mode, keep the local sample rules active — don't bake a site UUID (which would make
240206
// the guard fetch live Pulse rules instead of the bundled demo set).
241-
if (!opts.demo && bakeSiteUuid(cwd)) changed.push(GUARD_FILE);
207+
if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
242208
if (patchClient(cwd)) changed.push('src/integrations/supabase/client.ts');
243209
if (patchStart(cwd)) changed.push('src/start.ts');
244210
log(

‎src/protect/install/generic.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, statSync } from 'node:fs';
77
import { join } from 'node:path';
8-
import { read, templatesDir } from './util.js';
8+
import { bakeSiteUuid, read, templatesDir } from './util.js';
99
import type { WireOptions, VerifyResult } from './types.js';
1010

1111
const GUARD_MARKER = 'patchstack/guard';
@@ -21,6 +21,7 @@ export function scaffoldGeneric(cwd: string, opts: WireOptions, guardTemplate =
2121
mkdirSync(dst, { recursive: true });
2222
copyFileSync(join(templates, guardTemplate), join(dst, 'guard.ts'));
2323
const changed = [`${dir}/guard.ts`];
24+
if (!opts.demo) bakeSiteUuid(cwd, `${dir}/guard.ts`);
2425
const rulesDst = join(dst, 'rules.json');
2526
if (opts.demo || !existsSync(rulesDst)) {
2627
copyFileSync(join(templates, opts.demo ? 'demo-rules.json' : 'rules.json'), rulesDst);

‎src/protect/install/seam.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
77
import { join, dirname } from 'node:path';
8-
import { read, log, templatesDir } from './util.js';
8+
import { bakeSiteUuid, read, log, templatesDir } from './util.js';
99
import type { WireOptions, WireResult, VerifyResult } from './types.js';
1010

1111
export interface SeamSpec {
@@ -43,6 +43,7 @@ export function wireSeam(cwd: string, opts: WireOptions, spec: SeamSpec): WireRe
4343
}
4444

4545
copyFileSync(join(templates, spec.templateName), join(cwd, seamRel));
46+
if (!opts.demo) bakeSiteUuid(cwd, seamRel);
4647
changed.push(seamRel);
4748
log(existing ? `refreshed ${seamRel}` : `scaffolded ${seamRel}`);
4849
return { ok: true, changed: [...new Set(changed)] };

‎src/protect/install/util.ts‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Shared helpers for the `patchstack-connect protect` scaffolder (adapters + orchestrator).
22

3-
import { readFileSync, existsSync } from 'node:fs';
3+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
44
import { join, dirname } from 'node:path';
55
import { fileURLToPath } from 'node:url';
66

@@ -17,6 +17,49 @@ export function hasDependency(cwd: string, name: string): boolean {
1717
}
1818
}
1919

20+
const SITE_UUID_PLACEHOLDER = '__PATCHSTACK_SITE_UUID__';
21+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22+
23+
/**
24+
* Bake the site UUID written by `scan` into a managed runtime-guard template.
25+
*
26+
* The UUID is public project configuration (the disclosure widget exposes the
27+
* same value). Keeping the environment-variable fallback in the template lets
28+
* unscanned projects remain inert and lets deployments override it explicitly.
29+
*/
30+
export function bakeSiteUuid(cwd: string, guardRelPath: string): boolean {
31+
const rc = join(cwd, '.patchstackrc.json');
32+
if (!existsSync(rc)) {
33+
log('no .patchstackrc.json — guard uses PATCHSTACK_SITE_UUID env or the bundled fallback');
34+
return false;
35+
}
36+
37+
let uuid: unknown;
38+
try {
39+
uuid = JSON.parse(read(rc)).siteUuid;
40+
} catch {
41+
log('.patchstackrc.json unreadable — skipping site-UUID bake');
42+
return false;
43+
}
44+
45+
if (typeof uuid !== 'string' || !UUID_RE.test(uuid)) {
46+
log('.patchstackrc.json siteUuid missing or malformed — guard uses PATCHSTACK_SITE_UUID env or the bundled fallback');
47+
return false;
48+
}
49+
50+
const guardPath = join(cwd, guardRelPath);
51+
if (!existsSync(guardPath)) return false;
52+
const source = read(guardPath);
53+
if (!source.includes(SITE_UUID_PLACEHOLDER)) {
54+
log(`${guardRelPath} site UUID already baked`);
55+
return false;
56+
}
57+
58+
writeFileSync(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
59+
log(`baked site UUID into ${guardRelPath} — live rules from the Patchstack API`);
60+
return true;
61+
}
62+
2063
// Guard templates ship next to the built CLI (dist/protect/templates). Resolve for the built
2164
// layout (this code is bundled into dist/cli.js at the dist root → protect/templates) and the
2265
// source layout (this file lives in src/protect/install/ → ../templates).

‎src/protect/templates/astro-middleware.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import type { MiddlewareHandler } from "astro";
55
import { createProtection } from "@patchstack/connect/protect";
66
import fallbackRules from "./patchstack.rules.json";
77

8+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
9+
810
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
911
async function getProtection() {
1012
if (!_protection) {
1113
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
1214
const token = process.env.PATCHSTACK_WAF_TOKEN;
13-
const siteUuid = process.env.PATCHSTACK_SITE_UUID;
15+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
1416
const common = { mode, egress: true } as const;
1517
_protection = await createProtection(
1618
siteUuid

‎src/protect/templates/fastify-plugin.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import { createProtection } from "@patchstack/connect/protect";
66
import fallbackRules from "./rules.json";
77

8+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
9+
810
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
911
async function getProtection() {
1012
if (!_protection) {
1113
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
1214
const token = process.env.PATCHSTACK_WAF_TOKEN;
13-
const siteUuid = process.env.PATCHSTACK_SITE_UUID;
15+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
1416
const common = { mode, egress: true } as const;
1517
_protection = await createProtection(
1618
siteUuid

‎src/protect/templates/generic-guard.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import { createProtection } from "@patchstack/connect/protect";
88
import fallbackRules from "./rules.json";
99

10+
// Baked by `patchstack-connect protect` from .patchstackrc.json when available.
11+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
12+
1013
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
1114

1215
/** One memoized protection policy. Rules come from the Patchstack API per-site (cached); the
@@ -15,7 +18,7 @@ export async function getProtection() {
1518
if (!_protection) {
1619
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
1720
const token = process.env.PATCHSTACK_WAF_TOKEN;
18-
const siteUuid = process.env.PATCHSTACK_SITE_UUID;
21+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
1922
const common = { mode, egress: true } as const;
2023
_protection = await createProtection(
2124
siteUuid

‎src/protect/templates/next-middleware.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import { createProtection } from "@patchstack/connect/protect";
66
import fallbackRules from "./patchstack.rules.json";
77

8+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
9+
810
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
911
async function getProtection() {
1012
if (!_protection) {
1113
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
1214
const token = process.env.PATCHSTACK_WAF_TOKEN;
13-
const siteUuid = process.env.PATCHSTACK_SITE_UUID;
15+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
1416
const common = { mode, egress: true } as const;
1517
_protection = await createProtection(
1618
siteUuid

‎src/protect/templates/sveltekit-hooks.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import type { Handle } from "@sveltejs/kit";
55
import { createProtection } from "@patchstack/connect/protect";
66
import fallbackRules from "./patchstack.rules.json";
77

8+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
9+
810
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
911
async function getProtection() {
1012
if (!_protection) {
1113
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
1214
const token = process.env.PATCHSTACK_WAF_TOKEN;
13-
const siteUuid = process.env.PATCHSTACK_SITE_UUID;
15+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
1416
const common = { mode, egress: true } as const;
1517
_protection = await createProtection(
1618
siteUuid

0 commit comments

Comments
 (0)