Repository navigation
Expand file tree
/
Copy pathDeployGate.ts
More file actions
158 lines (137 loc) · 7.02 KB
/
Copy pathDeployGate.ts
File metadata and controls
158 lines (137 loc) · 7.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env bun
/**
* DeployGate — deterministic pre-deploy gate for daemon-data.json.
*
* Runs on EVERY deploy (wired into deploy.sh) regardless of how the data was
* edited — by an aggregator, by your AI, or by hand. Pure pattern matching, no
* LLM. Fails closed: any violation blocks the deploy.
*
* What it enforces:
* 1. TTL discipline — status requires `expires`; `now` requires
* now_meta.expires; offerings/requesting items with temporal language
* require `expires`; nothing already-expired ships (strip it instead).
* 2. Location granularity — no street addresses, ZIP codes, coordinates,
* or home-area strings anywhere in the payload; a non-default
* current_location requires location_meta.expires.
* 3. Real-time presence — "tonight / right now / I'm at" phrasing in
* status, now, location, or offerings requires an explicit
* realtime_approved: true on that item (deliberate, per-item opt-in).
* 4. Credential patterns — API keys/tokens never ship.
* 5. Feed sources — https public URLs only (no localhost / RFC1918).
*
* Usage:
* bun Tools/DeployGate.ts # gate ./daemon-data.json
* bun Tools/DeployGate.ts --fixture <file> # gate an alternate file (tests)
* bun Tools/DeployGate.ts --check # same checks, softer exit label
*/
import { readFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
// ─── Deterministic rule patterns ───
const STREET_ADDRESS = /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Ct|Court|Way|Pl|Place|Ter|Terrace)\b\.?/;
const CA_ZIP = /\b9[0-6]\d{3}(?:-\d{4})?\b/;
const COORDINATES = /-?\d{1,3}\.\d{3,}\s*,\s*-?\d{1,3}\.\d{3,}/;
// Home-area strings — put YOUR street/neighborhood/home-city here (this file lives in your PRIVATE instance repo).
const HOME_AREA = /\b(?:YOUR_STREET_NAME|YOUR_HOME_CITY)\b/i; // EDIT: your street/neighborhood/city strings
const REALTIME = /\b(?:tonight|right\s+now|currently\s+at|i'?m\s+at|today\s+at|on[-\s]site\s+at|this\s+evening)\b/i;
const TEMPORAL = /\b(?:tonight|today|tomorrow|this\s+(?:week|weekend|month|evening)|next\s+(?:week|weekend|month|two\s+weeks)|right\s+now)\b/i;
const CREDENTIALS = [
/sk-[a-zA-Z0-9_-]{20,}/,
/ghp_[a-zA-Z0-9]{36,}/,
/\b[A-Z_]+_(?:API_KEY|TOKEN|SECRET)\s*[=:]\s*\S+/,
];
const PRIVATE_URL = /(?:localhost|127\.0\.0\.1|\b10\.\d+\.\d+\.\d+|\b192\.168\.|\b172\.(?:1[6-9]|2\d|3[01])\.)/;
// ─── Gate ───
interface Violation {
rule: string;
where: string;
detail: string;
}
function expired(expires: unknown, now: number): boolean {
if (typeof expires !== "string") return false;
const t = Date.parse(expires);
return !Number.isNaN(t) && t < now;
}
function validExpiry(expires: unknown): boolean {
return typeof expires === "string" && !Number.isNaN(Date.parse(expires));
}
export function gate(data: Record<string, any>, nowMs = Date.now()): Violation[] {
const v: Violation[] = [];
const push = (rule: string, where: string, detail: string) => v.push({ rule, where, detail });
// 1. TTL discipline
if (data.status) {
if (!validExpiry(data.status.expires)) push("ttl", "status", "status requires a parseable `expires`");
else if (expired(data.status.expires, nowMs)) push("ttl", "status", `status expired ${data.status.expires} — remove or refresh it`);
}
if (data.now) {
if (!validExpiry(data.now_meta?.expires)) push("ttl", "now", "`now` requires now_meta.expires");
else if (expired(data.now_meta.expires, nowMs)) push("ttl", "now", `\`now\` expired ${data.now_meta.expires}`);
}
for (const key of ["offerings", "requesting"]) {
for (const [i, item] of (data[key] ?? []).entries()) {
const text = `${item.title ?? ""} ${item.description ?? ""}`;
if (TEMPORAL.test(text) && !validExpiry(item.expires)) {
push("ttl", `${key}[${i}]`, `time-bound language ("${text.match(TEMPORAL)?.[0]}") requires \`expires\``);
}
if (expired(item.expires, nowMs)) push("ttl", `${key}[${i}]`, `expired ${item.expires} — remove it`);
}
}
// 2 + 3. Location granularity and real-time presence — walk every string
const realtimeApproved = (holder: any) => holder?.realtime_approved === true;
const scanText = (text: string, where: string, holder: any) => {
if (STREET_ADDRESS.test(text)) push("location", where, `street address pattern: "${text.match(STREET_ADDRESS)?.[0]}"`);
if (CA_ZIP.test(text)) push("location", where, `ZIP code pattern: "${text.match(CA_ZIP)?.[0]}"`);
if (COORDINATES.test(text)) push("location", where, "coordinate pattern");
if (HOME_AREA.test(text)) push("location", where, `home-area string: "${text.match(HOME_AREA)?.[0]}"`);
if (REALTIME.test(text) && !realtimeApproved(holder)) {
push("realtime", where, `real-time presence phrasing ("${text.match(REALTIME)?.[0]}") without realtime_approved: true`);
}
};
if (typeof data.current_location === "string") {
scanText(data.current_location, "current_location", data.location_meta);
if (data.location_default && data.current_location !== data.location_default && !validExpiry(data.location_meta?.expires)) {
push("ttl", "current_location", "non-default location requires location_meta.expires");
}
}
if (data.status) scanText(`${data.status.headline ?? ""} ${data.status.detail ?? ""}`, "status", data.status);
if (typeof data.now === "string") scanText(data.now, "now", data.now_meta);
for (const key of ["offerings", "requesting"]) {
for (const [i, item] of (data[key] ?? []).entries()) {
scanText(`${item.title ?? ""} ${item.description ?? ""}`, `${key}[${i}]`, item);
}
}
// 4. Credentials — whole payload
const whole = JSON.stringify(data);
for (const pattern of CREDENTIALS) {
const m = whole.match(pattern);
if (m) push("credential", "payload", `credential pattern: "${m[0].slice(0, 12)}…"`);
}
// 5. Feed sources
for (const [i, src] of (data.feeds ?? []).entries()) {
for (const u of [src.url, src.profile_url]) {
if (u && (!u.startsWith("https://") || PRIVATE_URL.test(u))) {
push("feeds", `feeds[${i}]`, `source URL must be public https: ${u}`);
}
}
}
return v;
}
// ─── CLI ───
if (import.meta.main) {
const args = process.argv.slice(2);
const fixtureIdx = args.indexOf("--fixture");
const here = dirname(fileURLToPath(import.meta.url));
const file = fixtureIdx !== -1 && args[fixtureIdx + 1] ? args[fixtureIdx + 1] : join(here, "..", "daemon-data.json");
const data = JSON.parse(readFileSync(file, "utf-8"));
const violations = gate(data);
if (violations.length === 0) {
console.log(`DeployGate PASS — ${file}`);
process.exit(0);
}
console.error(`DeployGate FAIL — ${violations.length} violation(s) in ${file}:\n`);
for (const { rule, where, detail } of violations) {
console.error(` [${rule}] ${where}: ${detail}`);
}
console.error(args.includes("--check") ? "\nStale/unsafe data detected." : "\nDeploy blocked. Fix the data and re-run.");
process.exit(1);
}