@cosmicdrift/kumiko-framework 0.215.4 → 0.215.6

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.
@@ -1,232 +0,0 @@
1
- /**
2
- * Raw-SQL inventory — shared allowlist for `kumiko sql-inventory` and
3
- * `guard-raw-sql` (Phase 5). Scans TypeScript sources for escape-hatch patterns.
4
- *
5
- * Bun-only I/O: Bun.Glob + Bun.file (no node:fs, no node:path).
6
- */
7
-
8
- /** POSIX path join without Node path module. */
9
- export function joinPath(base: string, ...segments: string[]): string {
10
- return [base, ...segments]
11
- .join("/")
12
- .replace(/\/+/g, "/")
13
- .replace(/\/\.\//g, "/");
14
- }
15
-
16
- export type SqlInventoryKind = "unsafe" | "asRawClient" | "delete_from" | "execute";
17
-
18
- export type SqlInventoryHit = {
19
- readonly file: string;
20
- readonly line: number;
21
- readonly kind: SqlInventoryKind;
22
- readonly allowed: boolean;
23
- readonly snippet: string;
24
- };
25
-
26
- export type SqlInventoryReport = {
27
- readonly scannedAt: string;
28
- readonly root: string;
29
- readonly hits: readonly SqlInventoryHit[];
30
- readonly summary: {
31
- readonly total: number;
32
- readonly disallowed: number;
33
- readonly byKind: Readonly<Record<SqlInventoryKind, number>>;
34
- readonly byBucket: {
35
- readonly allowed: number;
36
- readonly tests: number;
37
- readonly disallowed: number;
38
- };
39
- };
40
- };
41
-
42
- /** Paths where `.unsafe()` / `asRawClient()` are permitted (Phase 5 guard). */
43
- export const RAW_SQL_ALLOWLIST: ReadonlyArray<RegExp> = [
44
- /\/packages\/framework\/src\/db\/queries\//,
45
- /\/packages\/framework\/src\/db\/migrate-runner\.ts$/,
46
- /\/packages\/framework\/src\/db\/schema-inspection\.ts$/,
47
- /\/packages\/framework\/src\/db\/render-ddl\.ts$/,
48
- /\/packages\/framework\/src\/db\/sql-inventory\.ts$/,
49
- /\/packages\/framework\/src\/bun-db\/query\.ts$/,
50
- /\/packages\/framework\/src\/testing\//,
51
- // ponytail: explicit enumeration — blanket regex auto-allowed any new .unsafe()
52
- /\/bundled-features\/src\/billing-foundation\/db\/queries\/subscription-projection\.ts$/,
53
- /\/bundled-features\/src\/config\/db\/queries\/resolver\.ts$/,
54
- /\/bundled-features\/src\/custom-fields\/db\/queries\/field-access\.ts$/,
55
- /\/bundled-features\/src\/custom-fields\/db\/queries\/projection\.ts$/,
56
- /\/bundled-features\/src\/custom-fields\/db\/queries\/quota\.ts$/,
57
- /\/bundled-features\/src\/custom-fields\/db\/queries\/retention\.ts$/,
58
- /\/bundled-features\/src\/custom-fields\/db\/queries\/user-data-rights\.ts$/,
59
- /\/bundled-features\/src\/delivery\/db\/queries\/preferences\.ts$/,
60
- /\/bundled-features\/src\/form-draft\/db\/queries\/cleanup\.ts$/,
61
- /\/bundled-features\/src\/form-draft\/db\/queries\/draft-count\.ts$/,
62
- /\/bundled-features\/src\/form-draft\/db\/queries\/owned-file-refs\.ts$/,
63
- /\/bundled-features\/src\/inbound-mail-foundation\/db\/queries\/inbound-projections\.ts$/,
64
- /\/bundled-features\/src\/secrets\/db\/queries\/read\.ts$/,
65
- /\/bundled-features\/src\/sessions\/db\/queries\/cleanup\.ts$/,
66
- /\/bundled-features\/src\/user\/db\/queries\/stream-tenant-backfill\.ts$/,
67
- /\/packages\/framework\/src\/engine\/steps\/unsafe-projection-/,
68
- /\/samples\/(apps|recipes)\/[^/]+\/src\/db\/queries\//,
69
- /\/bin\/commands\//,
70
- /\/scripts\/codemod-/,
71
- /\/__tests__\//,
72
- /\/scripts\/sql-inventory\.ts$/,
73
- /\/bin\/_lib\//,
74
- ];
75
-
76
- const SCAN_DIRS = ["packages", "samples", "scripts", "bin"] as const;
77
-
78
- const SKIP_PATH_PARTS = ["/node_modules/", "/dist/", "/.kumiko/"] as const;
79
-
80
- const PATTERNS: ReadonlyArray<{ readonly kind: SqlInventoryKind; readonly re: RegExp }> = [
81
- { kind: "unsafe", re: /\.unsafe\s*\(/ },
82
- { kind: "asRawClient", re: /asRawClient\s*\(/ },
83
- { kind: "delete_from", re: /DELETE\s+FROM/i },
84
- { kind: "execute", re: /\.execute\s*\(/ },
85
- ];
86
-
87
- const TS_GLOB = new Bun.Glob("**/*.{ts,tsx}");
88
-
89
- function normalizePathForMatch(filePath: string): string {
90
- return filePath.startsWith("/") ? filePath : `/${filePath}`;
91
- }
92
-
93
- export function isRawSqlAllowed(filePath: string): boolean {
94
- const normalized = normalizePathForMatch(filePath);
95
- return RAW_SQL_ALLOWLIST.some((re) => re.test(normalized));
96
- }
97
-
98
- function isTestPath(filePath: string): boolean {
99
- return /\/__tests__\//.test(normalizePathForMatch(filePath));
100
- }
101
-
102
- function bucketFor(hit: SqlInventoryHit): "allowed" | "tests" | "disallowed" {
103
- if (isTestPath(hit.file)) return "tests";
104
- if (hit.allowed) return "allowed";
105
- return "disallowed";
106
- }
107
-
108
- function shouldSkipRelativePath(rel: string): boolean {
109
- return SKIP_PATH_PARTS.some((part) => rel.includes(part));
110
- }
111
-
112
- function directoryExists(path: string): boolean {
113
- return Bun.spawnSync(["test", "-d", path]).exitCode === 0;
114
- }
115
-
116
- async function collectTsFiles(repoRoot: string): Promise<string[]> {
117
- const out: string[] = [];
118
- for (const sub of SCAN_DIRS) {
119
- const cwd = joinPath(repoRoot, sub);
120
- if (!directoryExists(cwd)) continue;
121
- for await (const rel of TS_GLOB.scan({ cwd, onlyFiles: true })) {
122
- const normalized = rel.replace(/\0/g, "");
123
- if (!normalized || shouldSkipRelativePath(normalized)) continue;
124
- out.push(joinPath(sub, normalized));
125
- }
126
- }
127
- return out;
128
- }
129
-
130
- function scanFileText(relPath: string, text: string, hits: SqlInventoryHit[]): void {
131
- const lines = text.split("\n");
132
- for (let i = 0; i < lines.length; i++) {
133
- const line = lines[i] ?? "";
134
- const trimmed = line.trim();
135
- if (
136
- trimmed.startsWith("//") ||
137
- trimmed.startsWith("*") ||
138
- trimmed.startsWith("/**") ||
139
- trimmed.startsWith("/*")
140
- ) {
141
- continue;
142
- }
143
- for (const { kind, re } of PATTERNS) {
144
- if (!re.test(line)) continue;
145
- hits.push({
146
- file: relPath,
147
- line: i + 1,
148
- kind,
149
- allowed: isRawSqlAllowed(relPath),
150
- snippet: trimmed.slice(0, 120),
151
- });
152
- }
153
- }
154
- }
155
-
156
- export async function scanRepo(repoRoot: string): Promise<SqlInventoryReport> {
157
- const relFiles = await collectTsFiles(repoRoot);
158
- const hits: SqlInventoryHit[] = [];
159
-
160
- for (const rel of relFiles) {
161
- const abs = joinPath(repoRoot, rel);
162
- const text = await Bun.file(abs).text();
163
- scanFileText(rel, text, hits);
164
- }
165
-
166
- const byKind: Record<SqlInventoryKind, number> = {
167
- unsafe: 0,
168
- asRawClient: 0,
169
- delete_from: 0,
170
- execute: 0,
171
- };
172
- let disallowed = 0;
173
- const byBucket = { allowed: 0, tests: 0, disallowed: 0 };
174
- for (const h of hits) {
175
- byKind[h.kind]++;
176
- const b = bucketFor(h);
177
- byBucket[b]++;
178
- if (b === "disallowed") disallowed++;
179
- }
180
-
181
- return {
182
- scannedAt: new Date().toISOString(),
183
- root: repoRoot,
184
- hits,
185
- summary: {
186
- total: hits.length,
187
- disallowed,
188
- byKind,
189
- byBucket,
190
- },
191
- };
192
- }
193
-
194
- // Serialize for the checked-in baseline. `root` (absolute scan path) and
195
- // `scannedAt` (run timestamp) are machine-/run-specific noise that churned the
196
- // baseline on every regen; --compare-baseline reads only summary.disallowed.
197
- // Pin them to stable placeholders so the committed file is reproducible.
198
- export function toBaselineJson(report: SqlInventoryReport): string {
199
- const stable: SqlInventoryReport = { ...report, root: ".", scannedAt: "" };
200
- return `${JSON.stringify(stable, null, 2)}\n`;
201
- }
202
-
203
- export function formatReport(report: SqlInventoryReport): string {
204
- const lines: string[] = [
205
- "--- sql inventory ---",
206
- ` scanned: ${report.scannedAt}`,
207
- ` root: ${report.root}`,
208
- ` total: ${report.summary.total}`,
209
- ` allowed: ${report.summary.byBucket.allowed}`,
210
- ` tests: ${report.summary.byBucket.tests}`,
211
- ` disallowed:${report.summary.disallowed}`,
212
- ` unsafe: ${report.summary.byKind.unsafe}`,
213
- ` asRawClient:${report.summary.byKind.asRawClient}`,
214
- ` DELETE FROM strings: ${report.summary.byKind.delete_from}`,
215
- ` .execute: ${report.summary.byKind.execute}`,
216
- "---",
217
- ];
218
-
219
- const bad = report.hits.filter((h) => bucketFor(h) === "disallowed");
220
- if (bad.length === 0) {
221
- lines.push(" (no disallowed production hits)");
222
- } else {
223
- lines.push(" disallowed (production):");
224
- for (const h of bad.slice(0, 40)) {
225
- lines.push(` ${h.kind.padEnd(12)} ${h.file}:${h.line} ${h.snippet}`);
226
- }
227
- if (bad.length > 40) {
228
- lines.push(` … +${bad.length - 40} more`);
229
- }
230
- }
231
- return lines.join("\n");
232
- }