Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./protect": {
"types": "./dist/protect.d.ts",
"import": "./dist/protect.js",
"require": "./dist/protect.cjs"
}
},
"bin": {
Expand Down
7 changes: 6 additions & 1 deletion scripts/copy-protect-templates.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// Copy the runtime-guard templates next to the built CLI so `patchstack-connect protect`
// can scaffold them. Runs AFTER tsup (post-build), so tsup's async .d.ts pass can't clobber
// the .d.ts templates (which it does if we copy via tsup's onSuccess).
import { cpSync, mkdirSync } from 'node:fs';
import { cpSync, mkdirSync, copyFileSync } from 'node:fs';

mkdirSync('dist/protect', { recursive: true });
cpSync('src/protect/templates', 'dist/protect/templates', { recursive: true });
console.log('copied protect templates -> dist/protect/templates');

// Ship the hand-authored declarations for the `@patchstack/connect/protect` subpath (the runtime
// is plain JS, so tsup doesn't emit these). Referenced by the "./protect" export's `types`.
copyFileSync('src/protect/protect.d.ts', 'dist/protect.d.ts');
console.log('copied protect types -> dist/protect.d.ts');
10 changes: 5 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Usage:
patchstack-connect status [options] Show current configuration
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
build fingerprint (run as a postbuild step)
patchstack-connect protect [--manifest] Install runtime protection (the guard) into
a server-side JS app. --manifest only
regenerates the package manifest (prebuild).
patchstack-connect protect Install always-on runtime protection (the
guard) into a TanStack Start + Supabase app.
Covers the browser + server-function paths.
patchstack-connect guide Print the full setup guide for AI coding
agents (also at https://patchstack.com/install.txt)
patchstack-connect help Print this message
Expand Down Expand Up @@ -196,10 +196,10 @@ async function runScan(args: ParsedArgs): Promise<number> {
return 0;
}

async function runProtectCommand(args: ParsedArgs): Promise<number> {
async function runProtectCommand(_args: ParsedArgs): Promise<number> {
// Best-effort: like mark-build, this runs during builds and must never fail one.
try {
runProtect(process.cwd(), { manifestOnly: args.flags.get('manifest') === true });
runProtect(process.cwd());
} catch (err) {
console.warn(`patchstack protect: skipped (${(err as Error).message}).`);
}
Expand Down
82 changes: 82 additions & 0 deletions src/protect/engine/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const DEFAULT_BASE_URL = 'https://api.patchstack.com';
const DEFAULT_CACHE_TTL = 300_000;

export class PatchstackRuleClient {
#token;
#baseUrl;
#cacheTtl;
#cache = null;
#cacheTime = null;

constructor({ token, baseUrl, cacheTtl } = {}) {
this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL;

if (!this.#token) {
throw new Error('Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.');
}
}

async getRules() {
const now = Date.now();

if (this.#cache && this.#cacheTime && (now - this.#cacheTime) < this.#cacheTtl) {
return this.#cache;
}

const url = `${this.#baseUrl}/api/get-rules/3`;

try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.#token}`,
'LicenseID': '1' // Hard-coded, is never actually used but needed by the API
},
body: JSON.stringify({}),
signal: AbortSignal.timeout(30_000)
});

if (!response.ok) {
return {
success: false,
error: `API returned ${response.status}: ${response.statusText}`,
firewall: [],
whitelists: [],
whitelist_keys: {}
};
}

const data = await response.json();

const result = {
success: true,
firewall: Array.isArray(data.firewall) ? data.firewall : [],
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
whitelist_keys: data.whitelist_keys ?? {}
};

this.#cache = result;
this.#cacheTime = now;

return result;
} catch (err) {
return {
success: false,
error: err.name === 'TimeoutError'
? 'Request timed out'
: err.message,
firewall: [],
whitelists: [],
whitelist_keys: {}
};
}
}

clearCache() {
this.#cache = null;
this.#cacheTime = null;
}
}
Loading
Loading