@viberaven/cli 1.6.12 → 1.7.1
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.
- package/AGENTS.md +64 -64
- package/LICENSE +21 -21
- package/README.md +67 -67
- package/SECURITY.md +59 -59
- package/dist/cli.js +5025 -1958
- package/dist/console/app.js +251 -251
- package/dist/console/index.html +46 -46
- package/dist/console/styles.css +267 -267
- package/dist/github-ci.js +1645 -0
- package/dist/playbooks/_stack-default.json +11 -11
- package/dist/playbooks/auth-supabase.json +31 -31
- package/dist/playbooks/stripe.json +66 -66
- package/dist/playbooks/supabase.json +51 -51
- package/dist/playbooks/vercel.json +42 -42
- package/dist/report/assets/provider-authjs.svg +5 -5
- package/dist/report/assets/provider-aws.svg +5 -5
- package/dist/report/assets/provider-logrocket.svg +4 -4
- package/dist/report/report-cli.css +1466 -1466
- package/dist/report/station.css +11854 -11854
- package/dist/report/station.js +8319 -8319
- package/dist/stack/app.js +14 -14
- package/dist/stack/scene.css +522 -484
- package/dist/stack/stack.css +1691 -1691
- package/dist/vendor/anime.umd.min.js +7 -7
- package/package.json +96 -95
- package/playbooks/_stack-default.json +11 -11
- package/playbooks/auth-supabase.json +31 -31
- package/playbooks/stripe.json +66 -66
- package/playbooks/supabase.json +51 -51
- package/playbooks/vercel.json +42 -42
- package/schemas/permissions-matrix.schema.json +90 -90
- package/schemas/verification-plan.schema.json +350 -0
- package/schemas/verification-report.schema.json +550 -0
- package/templates/AGENTS.snippet.md +56 -56
- package/templates/CLAUDE.snippet.md +14 -14
- package/templates/CURSOR.snippet.md +14 -14
|
@@ -0,0 +1,1645 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/github-ci/main.ts
|
|
22
|
+
var main_exports = {};
|
|
23
|
+
__export(main_exports, {
|
|
24
|
+
main: () => main,
|
|
25
|
+
scan: () => scan,
|
|
26
|
+
scannerEnv: () => scannerEnv
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(main_exports);
|
|
29
|
+
var import_node_child_process6 = require("node:child_process");
|
|
30
|
+
var import_node_fs = require("node:fs");
|
|
31
|
+
var import_promises3 = require("node:fs/promises");
|
|
32
|
+
var import_node_os2 = require("node:os");
|
|
33
|
+
var import_node_path4 = require("node:path");
|
|
34
|
+
|
|
35
|
+
// src/commands/checkChange.ts
|
|
36
|
+
var import_node_child_process4 = require("node:child_process");
|
|
37
|
+
var import_promises2 = require("node:fs/promises");
|
|
38
|
+
var import_node_os = require("node:os");
|
|
39
|
+
var import_node_path2 = require("node:path");
|
|
40
|
+
var import_node_util4 = require("node:util");
|
|
41
|
+
|
|
42
|
+
// ../../src/station/sifgTemplates.ts
|
|
43
|
+
var BLOCKED_PRODUCT_GLOBS = ["landing/**", "services/api/**", "marketing/**"];
|
|
44
|
+
var TEMPLATES = [
|
|
45
|
+
{
|
|
46
|
+
id: "template:payments:stripe:webhook-ingress",
|
|
47
|
+
label: "Stripe webhook ingress",
|
|
48
|
+
registryProvider: "stripe",
|
|
49
|
+
providerKey: "stripe-payments",
|
|
50
|
+
area: "payments",
|
|
51
|
+
sourceMatchers: [
|
|
52
|
+
{ kind: "route-handler", framework: "nextjs-app-router", routePattern: "/api/**/stripe/**", method: "POST" },
|
|
53
|
+
{ kind: "sdk-symbol", importName: "stripe", member: "webhooks.constructEvent" }
|
|
54
|
+
],
|
|
55
|
+
requiredGuards: [
|
|
56
|
+
{
|
|
57
|
+
kind: "signature-verifier",
|
|
58
|
+
symbols: ["stripe.webhooks.constructEvent"],
|
|
59
|
+
requiresEnvNames: ["STRIPE_WEBHOOK_SECRET"],
|
|
60
|
+
mustPrecede: ["database-write", "entitlement-update", "provider-mutation"]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
kind: "idempotency-guard",
|
|
64
|
+
symbols: ["event.id", "idempotencyKey", "processed_events"],
|
|
65
|
+
mustPrecede: ["entitlement-update"]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
forbiddenFlows: [
|
|
69
|
+
{ from: "request-body", to: "database-write", unlessGuardedBy: "signature-verifier" },
|
|
70
|
+
{ from: "env:STRIPE_SECRET_KEY", to: "client-bundle", severity: "critical" }
|
|
71
|
+
],
|
|
72
|
+
repoFixPolicy: {
|
|
73
|
+
allowedFileGlobs: ["app/**/route.ts", "src/server/**", "lib/server/**"],
|
|
74
|
+
blockedFileGlobs: BLOCKED_PRODUCT_GLOBS,
|
|
75
|
+
requiresApproval: true
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "template:auth:protected-data-access",
|
|
80
|
+
label: "Protected data access",
|
|
81
|
+
registryProvider: "clerk",
|
|
82
|
+
providerKey: "clerk-auth",
|
|
83
|
+
area: "auth",
|
|
84
|
+
sourceMatchers: [{ kind: "route-handler", routePattern: "/api/**", method: "ANY" }],
|
|
85
|
+
requiredGuards: [{ kind: "auth-guard", symbols: ["auth", "getServerSession", "currentUser"], mustPrecede: ["database-read", "database-write"] }],
|
|
86
|
+
forbiddenFlows: [{ from: "private-data", to: "response", unlessGuardedBy: "auth-guard" }],
|
|
87
|
+
repoFixPolicy: {
|
|
88
|
+
allowedFileGlobs: ["app/**/route.ts", "src/server/**", "lib/server/**", "middleware.ts", "src/middleware.ts"],
|
|
89
|
+
blockedFileGlobs: BLOCKED_PRODUCT_GLOBS,
|
|
90
|
+
requiresApproval: true
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "template:database:supabase:service-role-boundary",
|
|
95
|
+
label: "Supabase service role boundary",
|
|
96
|
+
registryProvider: "supabase",
|
|
97
|
+
providerKey: "supabase-database",
|
|
98
|
+
area: "database",
|
|
99
|
+
sourceMatchers: [{ kind: "env-read", envName: "SUPABASE_SERVICE_ROLE_KEY" }],
|
|
100
|
+
requiredGuards: [{ kind: "server-only-boundary", symbols: ["server-only"], mustPrecede: ["database-read", "database-write"] }],
|
|
101
|
+
forbiddenFlows: [{ from: "env:SUPABASE_SERVICE_ROLE_KEY", to: "client-bundle", severity: "critical" }],
|
|
102
|
+
repoFixPolicy: {
|
|
103
|
+
allowedFileGlobs: ["src/server/**", "lib/server/**", "app/**/route.ts", "supabase/**"],
|
|
104
|
+
blockedFileGlobs: BLOCKED_PRODUCT_GLOBS,
|
|
105
|
+
requiresApproval: true
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "template:security:public-route-abuse-guard",
|
|
110
|
+
label: "Public route abuse guard",
|
|
111
|
+
registryProvider: "rate-limit",
|
|
112
|
+
providerKey: "rate-limit-security",
|
|
113
|
+
area: "security",
|
|
114
|
+
sourceMatchers: [{ kind: "route-handler", routePattern: "/api/**", method: "ANY" }],
|
|
115
|
+
requiredGuards: [{ kind: "rate-limit-guard", symbols: ["ratelimit", "rateLimit", "upstash"], mustPrecede: ["provider-mutation", "database-write"] }],
|
|
116
|
+
forbiddenFlows: [{ from: "public-request", to: "expensive-sink", unlessGuardedBy: "rate-limit-guard" }],
|
|
117
|
+
repoFixPolicy: {
|
|
118
|
+
allowedFileGlobs: ["app/**/route.ts", "src/server/**", "lib/server/**"],
|
|
119
|
+
blockedFileGlobs: BLOCKED_PRODUCT_GLOBS,
|
|
120
|
+
requiresApproval: true
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "template:security:secret-boundary",
|
|
125
|
+
label: "Secret boundary",
|
|
126
|
+
registryProvider: "secrets-hygiene",
|
|
127
|
+
providerKey: "secrets-hygiene-security",
|
|
128
|
+
area: "security",
|
|
129
|
+
sourceMatchers: [{ kind: "env-read", envName: "*" }],
|
|
130
|
+
requiredGuards: [{ kind: "server-only-boundary", symbols: ["server-only"], mustPrecede: ["client-bundle"] }],
|
|
131
|
+
forbiddenFlows: [{ from: "env:*", to: "client-bundle", severity: "critical" }],
|
|
132
|
+
repoFixPolicy: {
|
|
133
|
+
allowedFileGlobs: ["src/**", "lib/**", "app/**"],
|
|
134
|
+
blockedFileGlobs: BLOCKED_PRODUCT_GLOBS,
|
|
135
|
+
requiresApproval: true
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "template:errorHandling:safe-error-response",
|
|
140
|
+
label: "Safe error response",
|
|
141
|
+
registryProvider: "sentry",
|
|
142
|
+
providerKey: "sentry-error-handling",
|
|
143
|
+
area: "errorHandling",
|
|
144
|
+
sourceMatchers: [{ kind: "throw-or-catch", routePattern: "/api/**" }],
|
|
145
|
+
requiredGuards: [{ kind: "error-capture", symbols: ["Sentry.captureException", "captureException"], mustPrecede: ["safe-response"] }],
|
|
146
|
+
forbiddenFlows: [{ from: "provider-error", to: "client-response", unlessGuardedBy: "safe-response" }],
|
|
147
|
+
repoFixPolicy: {
|
|
148
|
+
allowedFileGlobs: ["app/**/route.ts", "src/server/**", "lib/server/**"],
|
|
149
|
+
blockedFileGlobs: BLOCKED_PRODUCT_GLOBS,
|
|
150
|
+
requiresApproval: true
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
];
|
|
154
|
+
var SIFG_TEMPLATE_IDS = TEMPLATES.map((template) => template.id);
|
|
155
|
+
function allSifgTemplates() {
|
|
156
|
+
return TEMPLATES.map((template) => ({
|
|
157
|
+
...template,
|
|
158
|
+
sourceMatchers: template.sourceMatchers.map((matcher) => ({ ...matcher })),
|
|
159
|
+
requiredGuards: template.requiredGuards.map((guard) => ({
|
|
160
|
+
...guard,
|
|
161
|
+
symbols: [...guard.symbols],
|
|
162
|
+
requiresEnvNames: guard.requiresEnvNames ? [...guard.requiresEnvNames] : void 0,
|
|
163
|
+
mustPrecede: [...guard.mustPrecede]
|
|
164
|
+
})),
|
|
165
|
+
forbiddenFlows: template.forbiddenFlows.map((flow) => ({ ...flow })),
|
|
166
|
+
repoFixPolicy: {
|
|
167
|
+
allowedFileGlobs: [...template.repoFixPolicy.allowedFileGlobs],
|
|
168
|
+
blockedFileGlobs: [...template.repoFixPolicy.blockedFileGlobs],
|
|
169
|
+
requiresApproval: true
|
|
170
|
+
}
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
function sifgTemplatesForRegistryProviderArea(registryProvider, area) {
|
|
174
|
+
return allSifgTemplates().filter(
|
|
175
|
+
(template) => template.registryProvider === registryProvider && template.area === area
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ../../src/station/providerRegistry.ts
|
|
180
|
+
var PROVIDERS = [
|
|
181
|
+
provider("supabase", "Supabase", ["supabase"], ["database", "landing"], ["database", "auth"], "supabase", {
|
|
182
|
+
docsUrl: "https://supabase.com/docs",
|
|
183
|
+
dashboardUrl: "https://supabase.com/dashboard",
|
|
184
|
+
mcp: {
|
|
185
|
+
label: "Supabase",
|
|
186
|
+
serverName: "supabase",
|
|
187
|
+
vscodeServer: { type: "http", url: "https://mcp.supabase.com/mcp?read_only=true" },
|
|
188
|
+
cursorServer: { url: "https://mcp.supabase.com/mcp?read_only=true" },
|
|
189
|
+
keyInstructions: "For hosted Supabase MCP, no API key goes in this file. Your IDE opens browser OAuth after you add the server. A Supabase access token is only needed for CI/manual-header setups, where it is passed as Authorization: Bearer ${SUPABASE_ACCESS_TOKEN}."
|
|
190
|
+
},
|
|
191
|
+
verification: { supportsReadOnly: true }
|
|
192
|
+
}),
|
|
193
|
+
provider("supabase-auth", "Supabase Auth", ["supabaseauth"], ["auth"], [], "supabase", {
|
|
194
|
+
docsUrl: "https://supabase.com/docs/guides/auth",
|
|
195
|
+
dashboardUrl: "https://supabase.com/dashboard"
|
|
196
|
+
}),
|
|
197
|
+
provider("clerk", "Clerk", ["clerk"], ["auth"], ["auth"], "clerk", {
|
|
198
|
+
docsUrl: "https://clerk.com/docs",
|
|
199
|
+
dashboardUrl: "https://dashboard.clerk.com",
|
|
200
|
+
mcp: {
|
|
201
|
+
label: "Clerk",
|
|
202
|
+
serverName: "clerk",
|
|
203
|
+
vscodeServer: { type: "http", url: "https://mcp.clerk.com/mcp" },
|
|
204
|
+
cursorServer: { url: "https://mcp.clerk.com/mcp" },
|
|
205
|
+
keyInstructions: "Clerk MCP uses your Clerk credentials. If your IDE asks for a token, create one from the Clerk Dashboard developer settings."
|
|
206
|
+
},
|
|
207
|
+
verification: { supportsReadOnly: true }
|
|
208
|
+
}),
|
|
209
|
+
provider("authjs", "Auth.js", ["authjs", "auth", "nextauth"], ["auth"], ["auth"], "authjs", {
|
|
210
|
+
docsUrl: "https://authjs.dev"
|
|
211
|
+
}),
|
|
212
|
+
provider("auth0", "Auth0", ["auth0"], ["auth"], ["auth"], "auth0", {
|
|
213
|
+
docsUrl: "https://auth0.com/docs",
|
|
214
|
+
dashboardUrl: "https://manage.auth0.com"
|
|
215
|
+
}),
|
|
216
|
+
provider("better-auth", "Better Auth", ["betterauth", "better-auth"], ["auth"], ["auth"], "better-auth", {
|
|
217
|
+
docsUrl: "https://www.better-auth.com/docs"
|
|
218
|
+
}),
|
|
219
|
+
provider("firebase", "Firebase", ["firebase", "firestore"], ["database"], ["database"], "firebase", {
|
|
220
|
+
docsUrl: "https://firebase.google.com/docs",
|
|
221
|
+
dashboardUrl: "https://console.firebase.google.com"
|
|
222
|
+
}),
|
|
223
|
+
provider("neon", "Neon", ["neon"], ["database"], ["database"], "neon", {
|
|
224
|
+
docsUrl: "https://neon.tech/docs",
|
|
225
|
+
dashboardUrl: "https://console.neon.tech",
|
|
226
|
+
mcp: {
|
|
227
|
+
label: "Neon",
|
|
228
|
+
serverName: "neon",
|
|
229
|
+
vscodeServer: { type: "http", url: "https://mcp.neon.tech/mcp" },
|
|
230
|
+
cursorServer: { url: "https://mcp.neon.tech/mcp" },
|
|
231
|
+
keyInstructions: "Neon MCP normally opens browser OAuth. Use Neon project/database credentials only when your IDE asks for them."
|
|
232
|
+
},
|
|
233
|
+
verification: { supportsReadOnly: true }
|
|
234
|
+
}),
|
|
235
|
+
provider("planetscale", "PlanetScale", ["planetscale"], ["database"], ["database"], "planetscale", {
|
|
236
|
+
docsUrl: "https://planetscale.com/docs",
|
|
237
|
+
dashboardUrl: "https://app.planetscale.com",
|
|
238
|
+
mcp: {
|
|
239
|
+
label: "PlanetScale",
|
|
240
|
+
serverName: "planetscale",
|
|
241
|
+
vscodeServer: { type: "http", url: "https://mcp.pscale.dev/mcp/planetscale" },
|
|
242
|
+
cursorServer: { url: "https://mcp.pscale.dev/mcp/planetscale" },
|
|
243
|
+
keyInstructions: "PlanetScale MCP normally opens browser OAuth. Use a PlanetScale service token only for local/manual fallback setups."
|
|
244
|
+
},
|
|
245
|
+
verification: { supportsReadOnly: true }
|
|
246
|
+
}),
|
|
247
|
+
provider("mongodb", "MongoDB Atlas", ["mongodb", "mongodbatlas"], ["database"], ["database"], "mongodb", {
|
|
248
|
+
docsUrl: "https://www.mongodb.com/docs",
|
|
249
|
+
dashboardUrl: "https://cloud.mongodb.com",
|
|
250
|
+
mcp: {
|
|
251
|
+
label: "MongoDB",
|
|
252
|
+
serverName: "mongodb",
|
|
253
|
+
vscodeServer: { type: "stdio", command: "npx", args: ["-y", "mongodb-mcp-server", "--connectionString", "mongodb://localhost:27017/myDatabase", "--readOnly"] },
|
|
254
|
+
cursorServer: { command: "npx", args: ["-y", "mongodb-mcp-server", "--connectionString", "mongodb://localhost:27017/myDatabase", "--readOnly"] },
|
|
255
|
+
keyInstructions: "MongoDB MCP runs locally. Replace the connection string placeholder with a local or Atlas connection string and keep readOnly until you trust the workflow."
|
|
256
|
+
},
|
|
257
|
+
verification: { supportsReadOnly: false }
|
|
258
|
+
}),
|
|
259
|
+
provider("turso", "Turso", ["turso"], ["database"], ["database"], "turso", {
|
|
260
|
+
docsUrl: "https://docs.turso.tech",
|
|
261
|
+
dashboardUrl: "https://app.turso.tech"
|
|
262
|
+
}),
|
|
263
|
+
provider("stripe", "Stripe", ["stripe"], ["payments"], ["payments"], "stripe", {
|
|
264
|
+
docsUrl: "https://docs.stripe.com",
|
|
265
|
+
dashboardUrl: "https://dashboard.stripe.com",
|
|
266
|
+
mcp: {
|
|
267
|
+
label: "Stripe",
|
|
268
|
+
serverName: "stripe",
|
|
269
|
+
vscodeServer: { type: "http", url: "https://mcp.stripe.com" },
|
|
270
|
+
cursorServer: { url: "https://mcp.stripe.com" },
|
|
271
|
+
keyInstructions: "Stripe remote MCP normally authenticates with OAuth in the IDE. For local or custom bearer-token setups, use a restricted Stripe secret key from Developers > API keys."
|
|
272
|
+
},
|
|
273
|
+
verification: { supportsReadOnly: true }
|
|
274
|
+
}),
|
|
275
|
+
provider("paddle", "Paddle", ["paddle"], ["payments"], ["payments"], "paddle", {
|
|
276
|
+
docsUrl: "https://developer.paddle.com",
|
|
277
|
+
dashboardUrl: "https://vendors.paddle.com",
|
|
278
|
+
mcp: {
|
|
279
|
+
label: "Paddle",
|
|
280
|
+
serverName: "paddle",
|
|
281
|
+
vscodeServer: { type: "stdio", command: "npx", args: ["-y", "@paddle/paddle-mcp", "--api-key=YOUR_API_KEY", "--environment=sandbox", "--tools=non-destructive"] },
|
|
282
|
+
cursorServer: { command: "npx", args: ["-y", "@paddle/paddle-mcp", "--api-key=YOUR_API_KEY", "--environment=sandbox", "--tools=non-destructive"] },
|
|
283
|
+
keyInstructions: "Paddle MCP needs your Paddle authentication. Replace YOUR_API_KEY with a sandbox or production API key before enabling write-capable tools."
|
|
284
|
+
},
|
|
285
|
+
verification: { supportsReadOnly: false }
|
|
286
|
+
}),
|
|
287
|
+
provider("polar", "Polar", ["polar", "polarsh"], ["payments"], ["payments"], "polar", {
|
|
288
|
+
docsUrl: "https://polar.sh/docs",
|
|
289
|
+
dashboardUrl: "https://polar.sh/dashboard",
|
|
290
|
+
verification: { supportsReadOnly: false }
|
|
291
|
+
}),
|
|
292
|
+
provider("lemon-squeezy", "Lemon Squeezy", ["lemonsqueezy", "lemon-squeezy", "lemon squeezy"], ["payments"], ["payments"], "lemon-squeezy", {
|
|
293
|
+
docsUrl: "https://docs.lemonsqueezy.com",
|
|
294
|
+
dashboardUrl: "https://app.lemonsqueezy.com"
|
|
295
|
+
}),
|
|
296
|
+
provider("vercel", "Vercel", ["vercel"], ["deployment"], ["deployment"], "vercel", {
|
|
297
|
+
docsUrl: "https://vercel.com/docs",
|
|
298
|
+
dashboardUrl: "https://vercel.com/dashboard",
|
|
299
|
+
mcp: {
|
|
300
|
+
label: "Vercel",
|
|
301
|
+
serverName: "vercel",
|
|
302
|
+
vscodeServer: { type: "http", url: "https://mcp.vercel.com" },
|
|
303
|
+
cursorServer: { url: "https://mcp.vercel.com" },
|
|
304
|
+
keyInstructions: "Vercel MCP is a remote OAuth server. Finish the browser sign-in your IDE opens; only use a Vercel token for a separate CLI/local fallback."
|
|
305
|
+
},
|
|
306
|
+
verification: { supportsReadOnly: true }
|
|
307
|
+
}),
|
|
308
|
+
provider("netlify", "Netlify", ["netlify"], ["deployment"], ["deployment"], "netlify", {
|
|
309
|
+
docsUrl: "https://docs.netlify.com",
|
|
310
|
+
dashboardUrl: "https://app.netlify.com",
|
|
311
|
+
mcp: {
|
|
312
|
+
label: "Netlify",
|
|
313
|
+
serverName: "netlify",
|
|
314
|
+
vscodeServer: { type: "stdio", command: "npx", args: ["-y", "@netlify/mcp"] },
|
|
315
|
+
cursorServer: { command: "npx", args: ["-y", "@netlify/mcp"] },
|
|
316
|
+
keyInstructions: "Netlify MCP uses Netlify authentication from the IDE or CLI. Finish the browser sign-in or token prompt it opens."
|
|
317
|
+
},
|
|
318
|
+
verification: { supportsReadOnly: false }
|
|
319
|
+
}),
|
|
320
|
+
provider("render", "Render", ["render", "rendercom"], ["deployment"], ["deployment"], "render", {
|
|
321
|
+
docsUrl: "https://render.com/docs",
|
|
322
|
+
dashboardUrl: "https://dashboard.render.com"
|
|
323
|
+
}),
|
|
324
|
+
provider("railway", "Railway", ["railway", "railwayapp"], ["deployment"], ["deployment"], "railway", {
|
|
325
|
+
docsUrl: "https://docs.railway.com",
|
|
326
|
+
dashboardUrl: "https://railway.com"
|
|
327
|
+
}),
|
|
328
|
+
provider("cloudflare", "Cloudflare", ["cloudflare", "cloudflarepages", "workers"], ["deployment"], ["deployment"], "cloudflare", {
|
|
329
|
+
docsUrl: "https://developers.cloudflare.com",
|
|
330
|
+
dashboardUrl: "https://dash.cloudflare.com",
|
|
331
|
+
mcp: {
|
|
332
|
+
label: "Cloudflare",
|
|
333
|
+
serverName: "cloudflare-api",
|
|
334
|
+
vscodeServer: { type: "http", url: "https://mcp.cloudflare.com/mcp" },
|
|
335
|
+
cursorServer: { url: "https://mcp.cloudflare.com/mcp" },
|
|
336
|
+
keyInstructions: "Cloudflare MCP uses Cloudflare account authentication. Finish browser sign-in or provide an API token only when prompted."
|
|
337
|
+
},
|
|
338
|
+
verification: { supportsReadOnly: true }
|
|
339
|
+
}),
|
|
340
|
+
provider("aws", "AWS", ["aws"], ["deployment"], ["deployment"], "aws", {
|
|
341
|
+
docsUrl: "https://docs.aws.amazon.com",
|
|
342
|
+
dashboardUrl: "https://console.aws.amazon.com"
|
|
343
|
+
}),
|
|
344
|
+
provider("sentry", "Sentry", ["sentry"], ["monitoring", "errorHandling"], ["monitoring"], "sentry", {
|
|
345
|
+
docsUrl: "https://docs.sentry.io",
|
|
346
|
+
dashboardUrl: "https://sentry.io",
|
|
347
|
+
mcp: {
|
|
348
|
+
label: "Sentry",
|
|
349
|
+
serverName: "sentry",
|
|
350
|
+
vscodeServer: { type: "http", url: "https://mcp.sentry.dev/mcp" },
|
|
351
|
+
cursorServer: { url: "https://mcp.sentry.dev/mcp" },
|
|
352
|
+
keyInstructions: "Sentry MCP is a remote MCP server. Finish the IDE/browser authentication flow; only use a Sentry auth token for separate local or CLI fallback setups."
|
|
353
|
+
},
|
|
354
|
+
verification: { supportsReadOnly: true }
|
|
355
|
+
}),
|
|
356
|
+
provider("posthog", "PostHog", ["posthog"], ["monitoring", "errorHandling", "landing"], ["monitoring"], "posthog", {
|
|
357
|
+
docsUrl: "https://posthog.com/docs",
|
|
358
|
+
dashboardUrl: "https://app.posthog.com",
|
|
359
|
+
mcp: {
|
|
360
|
+
label: "PostHog",
|
|
361
|
+
serverName: "posthog",
|
|
362
|
+
vscodeServer: { type: "http", url: "https://mcp.posthog.com/mcp" },
|
|
363
|
+
cursorServer: { url: "https://mcp.posthog.com/mcp" },
|
|
364
|
+
keyInstructions: "PostHog MCP opens an authentication flow. Use a PostHog personal API key only if the IDE or local fallback asks for one."
|
|
365
|
+
},
|
|
366
|
+
verification: { supportsReadOnly: true }
|
|
367
|
+
}),
|
|
368
|
+
provider("logrocket", "LogRocket", ["logrocket"], ["monitoring"], ["monitoring"], "logrocket", {
|
|
369
|
+
docsUrl: "https://docs.logrocket.com",
|
|
370
|
+
dashboardUrl: "https://app.logrocket.com"
|
|
371
|
+
}),
|
|
372
|
+
provider("github", "GitHub Actions", ["github", "githubactions"], ["testing"], [], "github", {
|
|
373
|
+
docsUrl: "https://docs.github.com/actions",
|
|
374
|
+
dashboardUrl: "https://github.com",
|
|
375
|
+
mcp: {
|
|
376
|
+
label: "GitHub",
|
|
377
|
+
serverName: "github",
|
|
378
|
+
vscodeServer: { type: "http", url: "https://api.githubcopilot.com/mcp/" },
|
|
379
|
+
cursorServer: { url: "https://api.githubcopilot.com/mcp/" },
|
|
380
|
+
keyInstructions: "GitHub MCP should use IDE/GitHub authentication. Use read-only repository, Actions, and branch-protection queries for verification."
|
|
381
|
+
},
|
|
382
|
+
verification: { supportsReadOnly: true }
|
|
383
|
+
}),
|
|
384
|
+
provider("playwright", "Playwright", ["playwright", "playwrighttest"], ["testing"], [], "playwright", {
|
|
385
|
+
docsUrl: "https://playwright.dev/docs/intro",
|
|
386
|
+
mcp: {
|
|
387
|
+
label: "Playwright",
|
|
388
|
+
serverName: "playwright",
|
|
389
|
+
vscodeServer: { type: "stdio", command: "npx", args: ["-y", "@playwright/mcp@latest"] },
|
|
390
|
+
cursorServer: { command: "npx", args: ["-y", "@playwright/mcp@latest"] },
|
|
391
|
+
keyInstructions: "Playwright MCP runs locally through npx and normally does not need an API key."
|
|
392
|
+
},
|
|
393
|
+
verification: { supportsReadOnly: false }
|
|
394
|
+
}),
|
|
395
|
+
provider("rate-limit", "Rate limiting", ["upstash", "upstashratelimit", "ratelimit", "ratelimiting", "expressratelimit"], ["security"], ["security"], "rate-limit", {
|
|
396
|
+
docsUrl: "https://upstash.com/docs/redis/sdks/ratelimit/overview",
|
|
397
|
+
dashboardUrl: "https://console.upstash.com",
|
|
398
|
+
mcp: {
|
|
399
|
+
label: "Upstash",
|
|
400
|
+
serverName: "upstash",
|
|
401
|
+
vscodeServer: { type: "stdio", command: "npx", args: ["-y", "@upstash/mcp-server@latest", "--email", "<UPSTASH_EMAIL>", "--api-key", "<UPSTASH_API_KEY>"] },
|
|
402
|
+
cursorServer: { command: "npx", args: ["-y", "@upstash/mcp-server@latest", "--email", "<UPSTASH_EMAIL>", "--api-key", "<UPSTASH_API_KEY>"] },
|
|
403
|
+
keyInstructions: "Upstash MCP needs your Upstash account email and API key. Replace the placeholders before enabling the server."
|
|
404
|
+
},
|
|
405
|
+
verification: { supportsReadOnly: false }
|
|
406
|
+
}),
|
|
407
|
+
provider("bot-protection", "Bot protection", ["botprotection", "cloudflareturnstile", "turnstile"], ["security"], ["security"], "bot-protection", {
|
|
408
|
+
docsUrl: "https://developers.cloudflare.com/turnstile",
|
|
409
|
+
dashboardUrl: "https://dash.cloudflare.com",
|
|
410
|
+
mcp: {
|
|
411
|
+
label: "Cloudflare",
|
|
412
|
+
serverName: "cloudflare-api",
|
|
413
|
+
vscodeServer: { type: "http", url: "https://mcp.cloudflare.com/mcp" },
|
|
414
|
+
cursorServer: { url: "https://mcp.cloudflare.com/mcp" },
|
|
415
|
+
keyInstructions: "Cloudflare MCP uses Cloudflare account authentication. Finish browser sign-in or provide an API token only when prompted."
|
|
416
|
+
},
|
|
417
|
+
verification: { supportsReadOnly: true }
|
|
418
|
+
}),
|
|
419
|
+
provider("secrets-hygiene", "Secrets hygiene", ["secretshygiene", "envhygiene"], ["security"], ["security"], "secrets-hygiene"),
|
|
420
|
+
provider("figma", "Figma", ["figma"], ["appFlow"], [], "figma", { docsUrl: "https://help.figma.com" }),
|
|
421
|
+
provider("storybook", "Storybook", ["storybook"], ["appFlow"], [], "storybook", { docsUrl: "https://storybook.js.org/docs" }),
|
|
422
|
+
provider("product-spec", "Product Spec", ["productspec", "prd"], ["appFlow"], [], "product-spec"),
|
|
423
|
+
provider("route-map", "Route Map", ["routemap", "routes"], ["appFlow"], [], "route-map"),
|
|
424
|
+
provider("react", "React", ["react", "reactjs"], ["frontend"], [], "react", { docsUrl: "https://react.dev" }),
|
|
425
|
+
provider("vue", "Vue", ["vue", "vuejs"], ["frontend"], [], "vue", { docsUrl: "https://vuejs.org/guide" }),
|
|
426
|
+
provider("svelte", "Svelte", ["svelte", "sveltekit"], ["frontend"], [], "svelte", { docsUrl: "https://svelte.dev/docs" }),
|
|
427
|
+
provider("angular", "Angular", ["angular"], ["frontend"], [], "angular", { docsUrl: "https://angular.dev" }),
|
|
428
|
+
provider("node", "Node.js", ["node", "nodejs", "node.js"], ["backend"], [], "nodejs", { docsUrl: "https://nodejs.org/docs/latest/api" }),
|
|
429
|
+
provider("python", "Python / FastAPI", ["python", "pythonfastapi", "fastapi"], ["backend"], [], "python", { docsUrl: "https://fastapi.tiangolo.com" }),
|
|
430
|
+
provider("rails", "Rails", ["rails", "rubyonrails"], ["backend"], [], "rails", { docsUrl: "https://guides.rubyonrails.org" }),
|
|
431
|
+
provider("go", "Go", ["go", "golang"], ["backend"], [], "go", { docsUrl: "https://go.dev/doc" }),
|
|
432
|
+
provider("vitest", "Vitest", ["vitest"], ["testing"], [], "vitest", { docsUrl: "https://vitest.dev" })
|
|
433
|
+
];
|
|
434
|
+
var PROVIDERS_BY_KEY = new Map(PROVIDERS.map((entry) => [entry.provider, entry]));
|
|
435
|
+
var PROVIDERS_BY_ALIAS = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const entry of PROVIDERS) {
|
|
437
|
+
PROVIDERS_BY_ALIAS.set(normalizeProviderToken(entry.provider), entry);
|
|
438
|
+
for (const alias of entry.aliases) {
|
|
439
|
+
PROVIDERS_BY_ALIAS.set(normalizeProviderToken(alias), entry);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function productionProviders() {
|
|
443
|
+
return PROVIDERS.filter(isProductionProviderSeed).map((entry) => entry.provider);
|
|
444
|
+
}
|
|
445
|
+
function productionProvidersByArea() {
|
|
446
|
+
return {
|
|
447
|
+
database: providersForProductionArea("database"),
|
|
448
|
+
auth: providersForProductionArea("auth"),
|
|
449
|
+
payments: providersForProductionArea("payments"),
|
|
450
|
+
deployment: providersForProductionArea("deployment"),
|
|
451
|
+
monitoring: providersForProductionArea("monitoring"),
|
|
452
|
+
security: providersForProductionArea("security")
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function provider(providerKey, label, aliases, areas, productionAreas, iconKey, extras = {}) {
|
|
456
|
+
const sifgTemplateIds = areas.flatMap((area) => sifgTemplatesForRegistryProviderArea(providerKey, area)).map((template) => template.id);
|
|
457
|
+
return {
|
|
458
|
+
provider: providerKey,
|
|
459
|
+
label,
|
|
460
|
+
aliases,
|
|
461
|
+
areas,
|
|
462
|
+
productionAreas,
|
|
463
|
+
iconKey,
|
|
464
|
+
verifiedAt: "2026-05-18",
|
|
465
|
+
...sifgTemplateIds.length > 0 ? { sifgTemplateIds } : {},
|
|
466
|
+
...extras
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function providersForProductionArea(area) {
|
|
470
|
+
return PROVIDERS.filter(isProductionProviderSeed).filter((entry) => entry.productionAreas.includes(area)).map((entry) => entry.provider);
|
|
471
|
+
}
|
|
472
|
+
function isProductionProviderSeed(entry) {
|
|
473
|
+
return entry.productionAreas.length > 0;
|
|
474
|
+
}
|
|
475
|
+
function normalizeProviderToken(provider2) {
|
|
476
|
+
return provider2.toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ../../src/station/productionConnections.ts
|
|
480
|
+
var PROVIDERS2 = productionProviders();
|
|
481
|
+
var PROVIDERS_BY_AREA = productionProvidersByArea();
|
|
482
|
+
var PROVIDER_RULES = freezeProviderRules([
|
|
483
|
+
{
|
|
484
|
+
area: "database",
|
|
485
|
+
provider: "supabase",
|
|
486
|
+
label: "Supabase",
|
|
487
|
+
packages: [/@supabase\//],
|
|
488
|
+
env: ["VITE_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_URL", "SUPABASE_SERVICE_ROLE_KEY"],
|
|
489
|
+
paths: [/supabase\/config\.toml$/, /(^|\/)(lib|utils)\/supabase\.[jt]s$/],
|
|
490
|
+
imports: ["@supabase/supabase-js", "@supabase/ssr"],
|
|
491
|
+
docs: ["supabase"]
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
area: "database",
|
|
495
|
+
provider: "firebase",
|
|
496
|
+
label: "Firebase",
|
|
497
|
+
packages: [/^firebase$/, /^firebase-admin$/, /@firebase\//],
|
|
498
|
+
env: ["FIREBASE_PROJECT_ID", "NEXT_PUBLIC_FIREBASE_PROJECT_ID", "VITE_FIREBASE_PROJECT_ID", "GOOGLE_APPLICATION_CREDENTIALS"],
|
|
499
|
+
paths: [/firebase/, /firestore/],
|
|
500
|
+
imports: ["firebase/app", "firebase/firestore", "firebase-admin"],
|
|
501
|
+
content: [{ pattern: /getFirestore\s*\(|collection\s*\(|firebase-admin/i, signal: "code: Firebase/Firestore usage" }],
|
|
502
|
+
docs: ["firebase", "firestore"]
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
area: "database",
|
|
506
|
+
provider: "neon",
|
|
507
|
+
label: "Neon",
|
|
508
|
+
packages: [/@neondatabase\//],
|
|
509
|
+
env: ["NEON_DATABASE_URL"],
|
|
510
|
+
imports: ["@neondatabase/serverless"],
|
|
511
|
+
docs: ["neon"]
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
area: "database",
|
|
515
|
+
provider: "planetscale",
|
|
516
|
+
label: "PlanetScale",
|
|
517
|
+
packages: [/@planetscale\/database/],
|
|
518
|
+
env: ["PLANETSCALE_DATABASE_URL"],
|
|
519
|
+
imports: ["@planetscale/database"],
|
|
520
|
+
docs: ["planetscale"]
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
area: "database",
|
|
524
|
+
provider: "mongodb",
|
|
525
|
+
label: "MongoDB",
|
|
526
|
+
packages: [/^mongodb$/, /^mongoose$/],
|
|
527
|
+
env: ["MONGODB_URI", "MONGODB_URL", "MONGODB_ATLAS_URI"],
|
|
528
|
+
imports: ["mongodb", "mongoose"],
|
|
529
|
+
docs: ["mongodb", "mongo atlas"]
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
area: "database",
|
|
533
|
+
provider: "turso",
|
|
534
|
+
label: "Turso",
|
|
535
|
+
packages: [/@libsql\/client/],
|
|
536
|
+
env: ["TURSO_DATABASE_URL", "TURSO_AUTH_TOKEN"],
|
|
537
|
+
imports: ["@libsql/client"],
|
|
538
|
+
docs: ["turso"]
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
area: "auth",
|
|
542
|
+
provider: "clerk",
|
|
543
|
+
label: "Clerk",
|
|
544
|
+
packages: [/@clerk\//],
|
|
545
|
+
env: ["CLERK_SECRET_KEY", "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"],
|
|
546
|
+
paths: [/middleware\.[jt]sx?$/],
|
|
547
|
+
imports: ["@clerk/nextjs", "@clerk/clerk-react", "@clerk/express"],
|
|
548
|
+
docs: ["clerk"]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
area: "auth",
|
|
552
|
+
provider: "authjs",
|
|
553
|
+
label: "Auth.js",
|
|
554
|
+
packages: [/^next-auth$/, /^@auth\//],
|
|
555
|
+
env: ["AUTH_SECRET", "NEXTAUTH_SECRET", "NEXTAUTH_URL"],
|
|
556
|
+
paths: [/api\/auth\//],
|
|
557
|
+
imports: ["next-auth", "@auth/core", "@auth/nextjs"],
|
|
558
|
+
content: [{ pattern: /NextAuth\s*\(|getServerSession|useSession\s*\(/i, signal: "code: Auth.js session or route handler" }],
|
|
559
|
+
docs: ["auth.js", "nextauth", "next-auth"]
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
area: "auth",
|
|
563
|
+
provider: "auth0",
|
|
564
|
+
label: "Auth0",
|
|
565
|
+
packages: [/@auth0\//],
|
|
566
|
+
env: ["AUTH0_SECRET", "AUTH0_ISSUER_BASE_URL", "AUTH0_CLIENT_ID", "AUTH0_CLIENT_SECRET", "AUTH0_DOMAIN"],
|
|
567
|
+
paths: [/api\/auth/, /auth0/],
|
|
568
|
+
imports: ["@auth0/nextjs-auth0", "@auth0/auth0-react", "express-openid-connect"],
|
|
569
|
+
content: [{ pattern: /handleAuth|withPageAuthRequired|withApiAuthRequired|getSession/i, signal: "code: Auth0 session or route protection" }],
|
|
570
|
+
docs: ["auth0"]
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
area: "auth",
|
|
574
|
+
provider: "better-auth",
|
|
575
|
+
label: "Better Auth",
|
|
576
|
+
packages: [/^better-auth$/],
|
|
577
|
+
env: ["BETTER_AUTH_SECRET", "BETTER_AUTH_URL", "AUTH_SECRET", "DATABASE_URL"],
|
|
578
|
+
paths: [/better-auth/, /auth\.[jt]s$/],
|
|
579
|
+
imports: ["better-auth"],
|
|
580
|
+
content: [{ pattern: /betterAuth\s*\(|auth\.api|auth\.handler/i, signal: "code: Better Auth config" }],
|
|
581
|
+
docs: ["better auth", "better-auth"]
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
area: "payments",
|
|
585
|
+
provider: "stripe",
|
|
586
|
+
label: "Stripe",
|
|
587
|
+
packages: [/^stripe$/, /@stripe\//],
|
|
588
|
+
env: ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET", "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY"],
|
|
589
|
+
paths: [/stripe.*webhook/, /webhook.*stripe/, /stripe.*checkout/, /checkout.*stripe/],
|
|
590
|
+
imports: ["stripe", "@stripe/stripe-js"],
|
|
591
|
+
docs: ["stripe"]
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
area: "payments",
|
|
595
|
+
provider: "paddle",
|
|
596
|
+
label: "Paddle",
|
|
597
|
+
packages: [/@paddle\//],
|
|
598
|
+
env: ["PADDLE_API_KEY", "PADDLE_WEBHOOK_SECRET", "NEXT_PUBLIC_PADDLE_CLIENT_TOKEN"],
|
|
599
|
+
paths: [/paddle.*webhook/, /webhook.*paddle/, /paddle.*checkout/, /checkout.*paddle/],
|
|
600
|
+
imports: ["@paddle/paddle-js", "@paddle/paddle-node-sdk"],
|
|
601
|
+
docs: ["paddle"]
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
area: "payments",
|
|
605
|
+
provider: "polar",
|
|
606
|
+
label: "Polar",
|
|
607
|
+
packages: [/@polar-sh\//],
|
|
608
|
+
env: ["POLAR_ACCESS_TOKEN", "POLAR_WEBHOOK_SECRET", "POLAR_PRODUCT_ID", "POLAR_PRO_PRODUCT_ID"],
|
|
609
|
+
paths: [/polar.*webhook/, /webhook.*polar/, /polar.*checkout/, /checkout.*polar/],
|
|
610
|
+
imports: ["@polar-sh/sdk"],
|
|
611
|
+
content: [{ pattern: /api\.polar\.sh|polar.*checkout|createCheckoutSession/i, signal: "code: Polar checkout or API usage" }],
|
|
612
|
+
docs: ["polar"]
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
area: "payments",
|
|
616
|
+
provider: "lemon-squeezy",
|
|
617
|
+
label: "Lemon Squeezy",
|
|
618
|
+
packages: [/@lemonsqueezy\//, /^lemonsqueezy\.ts$/],
|
|
619
|
+
env: ["LEMON_SQUEEZY_API_KEY", "LEMONSQUEEZY_API_KEY", "LEMON_SQUEEZY_WEBHOOK_SECRET", "LEMON_SQUEEZY_STORE_ID", "LEMON_SQUEEZY_VARIANT_ID"],
|
|
620
|
+
paths: [/lemon.*webhook/, /webhook.*lemon/, /lemon.*checkout/, /checkout.*lemon/, /lemonsqueezy/],
|
|
621
|
+
imports: ["@lemonsqueezy/lemonsqueezy.js", "lemonsqueezy.ts"],
|
|
622
|
+
content: [{ pattern: /api\.lemonsqueezy\.com|lemon.*checkout|checkout_url|variant_id/i, signal: "code: Lemon Squeezy checkout or API usage" }],
|
|
623
|
+
docs: ["lemon squeezy", "lemonsqueezy"]
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
area: "deployment",
|
|
627
|
+
provider: "vercel",
|
|
628
|
+
label: "Vercel",
|
|
629
|
+
packages: [/@vercel\//],
|
|
630
|
+
env: ["VERCEL_TOKEN", "VERCEL_PROJECT_ID", "VERCEL_ORG_ID"],
|
|
631
|
+
paths: [/vercel\.json$/],
|
|
632
|
+
docs: ["vercel"]
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
area: "deployment",
|
|
636
|
+
provider: "netlify",
|
|
637
|
+
label: "Netlify",
|
|
638
|
+
packages: [/@netlify\//],
|
|
639
|
+
env: ["NETLIFY_AUTH_TOKEN", "NETLIFY_SITE_ID"],
|
|
640
|
+
paths: [/netlify\.toml$/, /\.netlify\//, /(^|\/)_redirects$/],
|
|
641
|
+
imports: ["@netlify/functions"],
|
|
642
|
+
docs: ["netlify"]
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
area: "deployment",
|
|
646
|
+
provider: "render",
|
|
647
|
+
label: "Render",
|
|
648
|
+
env: ["RENDER_API_KEY", "RENDER_SERVICE_ID"],
|
|
649
|
+
paths: [/render\.ya?ml$/, /(^|\/)\.render\//],
|
|
650
|
+
content: [{ pattern: /render\.yaml|render\.yml|render deploy|render service/i, signal: "code: Render deployment config" }],
|
|
651
|
+
docs: ["render.com", "render deployment"]
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
area: "deployment",
|
|
655
|
+
provider: "railway",
|
|
656
|
+
label: "Railway",
|
|
657
|
+
env: ["RAILWAY_TOKEN", "RAILWAY_PROJECT_ID", "RAILWAY_SERVICE_ID"],
|
|
658
|
+
paths: [/railway\.json$/, /nixpacks\.toml$/, /(^|\/)Procfile$/],
|
|
659
|
+
content: [{ pattern: /railway up|railway deploy|nixpacks|railway\.json/i, signal: "code: Railway deployment config" }],
|
|
660
|
+
docs: ["railway"]
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
area: "deployment",
|
|
664
|
+
provider: "cloudflare",
|
|
665
|
+
label: "Cloudflare",
|
|
666
|
+
packages: [/^wrangler$/, /@cloudflare\//],
|
|
667
|
+
env: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "CF_PAGES"],
|
|
668
|
+
paths: [/wrangler\.toml$/, /wrangler\.json$/, /(^|\/)_headers$/, /(^|\/)_redirects$/],
|
|
669
|
+
imports: ["@cloudflare/workers-types"],
|
|
670
|
+
content: [{ pattern: /compatibility_date|pages_build_output_dir|cloudflare pages|cloudflare workers/i, signal: "code: Cloudflare Pages/Workers config" }],
|
|
671
|
+
docs: ["cloudflare", "workers", "pages"]
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
area: "deployment",
|
|
675
|
+
provider: "aws",
|
|
676
|
+
label: "AWS",
|
|
677
|
+
packages: [/@aws-sdk\//, /^aws-cdk-lib$/, /^serverless$/],
|
|
678
|
+
env: ["AWS_REGION", "AWS_ACCESS_KEY_ID"],
|
|
679
|
+
paths: [/serverless\.ya?ml$/, /template\.ya?ml$/, /cdk\.json$/, /amplify\//],
|
|
680
|
+
imports: ["aws-sdk", "@aws-sdk/client"],
|
|
681
|
+
docs: ["aws", "amplify", "serverless"]
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
area: "monitoring",
|
|
685
|
+
provider: "sentry",
|
|
686
|
+
label: "Sentry",
|
|
687
|
+
packages: [/@sentry\//],
|
|
688
|
+
env: ["SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN", "SENTRY_AUTH_TOKEN"],
|
|
689
|
+
paths: [/sentry\.(client|server)\.config\.[jt]s$/, /instrumentation\.[jt]s$/],
|
|
690
|
+
imports: ["@sentry/nextjs", "@sentry/node", "@sentry/react"],
|
|
691
|
+
content: [{ pattern: /sentry\.init\s*\(/i, signal: "init: Sentry.init" }],
|
|
692
|
+
docs: ["sentry"]
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
area: "monitoring",
|
|
696
|
+
provider: "posthog",
|
|
697
|
+
label: "PostHog",
|
|
698
|
+
packages: [/^posthog-js$/, /^posthog-node$/],
|
|
699
|
+
env: ["POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST"],
|
|
700
|
+
imports: ["posthog-js", "posthog-node"],
|
|
701
|
+
content: [{ pattern: /posthog\.init\s*\(/i, signal: "init: posthog.init" }],
|
|
702
|
+
docs: ["posthog"]
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
area: "monitoring",
|
|
706
|
+
provider: "logrocket",
|
|
707
|
+
label: "LogRocket",
|
|
708
|
+
packages: [/^logrocket$/],
|
|
709
|
+
env: ["LOGROCKET_APP_ID", "NEXT_PUBLIC_LOGROCKET_APP_ID"],
|
|
710
|
+
imports: ["logrocket"],
|
|
711
|
+
content: [{ pattern: /logrocket\.init\s*\(/i, signal: "init: LogRocket.init" }],
|
|
712
|
+
docs: ["logrocket"]
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
area: "security",
|
|
716
|
+
provider: "rate-limit",
|
|
717
|
+
label: "Upstash rate limit",
|
|
718
|
+
packages: [/@upstash\/ratelimit/, /express-rate-limit/, /rate-limiter-flexible/, /@fastify\/rate-limit/],
|
|
719
|
+
env: ["UPSTASH_REDIS_REST_URL", "UPSTASH_REDIS_REST_TOKEN"],
|
|
720
|
+
paths: [/rate-?limit/, /ratelimit/],
|
|
721
|
+
imports: ["@upstash/ratelimit", "express-rate-limit", "rate-limiter-flexible", "@fastify/rate-limit"],
|
|
722
|
+
content: [{ pattern: /\brateLimit\b|\bRatelimit\b|\bToo many requests\b/i, signal: "code: rate limit guard" }],
|
|
723
|
+
docs: ["upstash", "rate limit"]
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
area: "security",
|
|
727
|
+
provider: "bot-protection",
|
|
728
|
+
label: "Cloudflare Turnstile",
|
|
729
|
+
packages: [/turnstile/],
|
|
730
|
+
env: ["NEXT_PUBLIC_TURNSTILE_SITE_KEY", "TURNSTILE_SECRET_KEY", "TURNSTILE_SITE_KEY"],
|
|
731
|
+
paths: [/turnstile/, /bot.?protection/],
|
|
732
|
+
imports: ["@marsidev/react-turnstile", "react-turnstile"],
|
|
733
|
+
content: [{ pattern: /\bturnstile\b|\bcf-turnstile\b|\brecaptcha\b|\bhcaptcha\b/i, signal: "code: bot protection guard" }],
|
|
734
|
+
docs: ["turnstile", "cloudflare turnstile", "bot protection"]
|
|
735
|
+
}
|
|
736
|
+
]);
|
|
737
|
+
function freezeProviderRules(rules) {
|
|
738
|
+
for (const rule of rules) {
|
|
739
|
+
Object.freeze(rule.packages ?? []);
|
|
740
|
+
Object.freeze(rule.env ?? []);
|
|
741
|
+
Object.freeze(rule.paths ?? []);
|
|
742
|
+
Object.freeze(rule.imports ?? []);
|
|
743
|
+
Object.freeze(rule.docs ?? []);
|
|
744
|
+
if (rule.content) {
|
|
745
|
+
for (const item of rule.content) {
|
|
746
|
+
Object.freeze(item);
|
|
747
|
+
}
|
|
748
|
+
Object.freeze(rule.content);
|
|
749
|
+
}
|
|
750
|
+
Object.freeze(rule);
|
|
751
|
+
}
|
|
752
|
+
return Object.freeze(rules);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/localStation.ts
|
|
756
|
+
var import_node_child_process3 = require("node:child_process");
|
|
757
|
+
var import_node_util3 = require("node:util");
|
|
758
|
+
|
|
759
|
+
// src/sanitizeArtifact.ts
|
|
760
|
+
var SENSITIVE_KEY = "DATABASE_URL|ACCESS_TOKEN|AUTHORIZATION|API_KEY|SECRET|SECRET_KEY|SERVICE_ROLE_KEY|TOKEN|PASSWORD|PRIVATE_KEY|CREDENTIALS?";
|
|
761
|
+
var SENSITIVE_ASSIGNMENT = new RegExp(
|
|
762
|
+
`\\b([A-Za-z0-9_]*(?:${SENSITIVE_KEY})[A-Za-z0-9_]*)\\s*=\\s*["']?[^"'\\s;,]+["']?`,
|
|
763
|
+
"gi"
|
|
764
|
+
);
|
|
765
|
+
var SENSITIVE_COLON_ASSIGNMENT = new RegExp(
|
|
766
|
+
`(["']?)\\b([A-Za-z0-9_]*(?:${SENSITIVE_KEY})[A-Za-z0-9_]*)\\1\\s*:\\s*(?:"([^"]*)"|'([^']*)'|([^\\s,;}\\]]+))`,
|
|
767
|
+
"gi"
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
// src/commands/audit.ts
|
|
771
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "build", "coverage"]);
|
|
772
|
+
var SOURCE_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
773
|
+
...SKIP_DIRS,
|
|
774
|
+
".viberaven",
|
|
775
|
+
".vercel",
|
|
776
|
+
".turbo",
|
|
777
|
+
".svelte-kit",
|
|
778
|
+
".nuxt",
|
|
779
|
+
".output",
|
|
780
|
+
".expo",
|
|
781
|
+
".cache",
|
|
782
|
+
".idea",
|
|
783
|
+
".vscode",
|
|
784
|
+
"out",
|
|
785
|
+
"vendor",
|
|
786
|
+
"ios",
|
|
787
|
+
"android"
|
|
788
|
+
]);
|
|
789
|
+
var SOURCE_LIMITS = {
|
|
790
|
+
/** Directory entries listed, across the whole walk. */
|
|
791
|
+
entries: 2e5,
|
|
792
|
+
depth: 25,
|
|
793
|
+
/** Files kept in memory. */
|
|
794
|
+
files: 5e3,
|
|
795
|
+
/** A code file up to this size is read to test for Supabase text; bigger ones are reported unread. */
|
|
796
|
+
fileBytes: 32e6,
|
|
797
|
+
/** Total size of the files kept. */
|
|
798
|
+
totalBytes: 64e6
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/databaseEvidence.ts
|
|
802
|
+
var import_node_child_process2 = require("node:child_process");
|
|
803
|
+
var import_node_util2 = require("node:util");
|
|
804
|
+
|
|
805
|
+
// src/safeGit.ts
|
|
806
|
+
var import_node_child_process = require("node:child_process");
|
|
807
|
+
var import_promises = require("node:fs/promises");
|
|
808
|
+
var import_node_path = require("node:path");
|
|
809
|
+
var import_node_util = require("node:util");
|
|
810
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
811
|
+
var NULL_DEVICE = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
812
|
+
var HARDENING_CONFIG = [
|
|
813
|
+
["core.fsmonitor", "false"],
|
|
814
|
+
["core.untrackedCache", "false"],
|
|
815
|
+
["core.hooksPath", NULL_DEVICE],
|
|
816
|
+
["log.showSignature", "false"],
|
|
817
|
+
["submodule.recurse", "false"]
|
|
818
|
+
];
|
|
819
|
+
var ENV_CONFIG_SINCE = [2, 31];
|
|
820
|
+
var SHOW_SCOPE_SINCE = [2, 26];
|
|
821
|
+
var WORKTREE_COMMANDS = /* @__PURE__ */ new Set(["status", "checkout", "switch", "restore", "reset", "stash", "add", "diff"]);
|
|
822
|
+
var WRITE_COMMANDS = /* @__PURE__ */ new Set(["checkout", "switch", "restore", "reset", "stash", "add"]);
|
|
823
|
+
var TREE_DIFF_OPTIONS = /* @__PURE__ */ new Set(["--no-color", "--shortstat", "--stat", "--numstat", "--name-only", "--name-status", "--patch", "-p", "--no-ext-diff", "--no-textconv"]);
|
|
824
|
+
var DIFF_COMMANDS = /* @__PURE__ */ new Set(["diff", "log", "show"]);
|
|
825
|
+
var TRUSTED_SCOPES = /* @__PURE__ */ new Set(["system", "global"]);
|
|
826
|
+
var QUERY_TIMEOUT_MS = 1500;
|
|
827
|
+
var SafeGitRefusal = class extends Error {
|
|
828
|
+
name = "SafeGitRefusal";
|
|
829
|
+
};
|
|
830
|
+
var listConfigWithGit = async (cwd, args, env) => (await execFileAsync("git", args, { cwd, env, timeout: QUERY_TIMEOUT_MS, windowsHide: true, encoding: "utf8" })).stdout;
|
|
831
|
+
var versionOverride;
|
|
832
|
+
var versionLookup;
|
|
833
|
+
var configLister = listConfigWithGit;
|
|
834
|
+
function atLeast(version, minimum) {
|
|
835
|
+
return version[0] !== minimum[0] ? version[0] > minimum[0] : version[1] >= minimum[1];
|
|
836
|
+
}
|
|
837
|
+
async function gitVersion() {
|
|
838
|
+
if (versionOverride) return versionOverride;
|
|
839
|
+
versionLookup ??= execFileAsync("git", ["--version"], { timeout: QUERY_TIMEOUT_MS * 2, windowsHide: true, encoding: "utf8" }).then(
|
|
840
|
+
({ stdout }) => {
|
|
841
|
+
const match = /(\d+)\.(\d+)/.exec(stdout);
|
|
842
|
+
if (!match) throw new Error(`Unrecognised git version: ${stdout.trim()}`);
|
|
843
|
+
return [Number(match[1]), Number(match[2])];
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
const lookup = versionLookup;
|
|
847
|
+
lookup.catch(() => {
|
|
848
|
+
if (versionLookup === lookup) versionLookup = void 0;
|
|
849
|
+
});
|
|
850
|
+
return lookup;
|
|
851
|
+
}
|
|
852
|
+
function baseEnv(touchesWorktree) {
|
|
853
|
+
return {
|
|
854
|
+
...process.env,
|
|
855
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
856
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
857
|
+
...touchesWorktree ? {} : { GIT_CONFIG_NOSYSTEM: "1" }
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function withEnvConfig(env, entries) {
|
|
861
|
+
const existing = Number.parseInt(env.GIT_CONFIG_COUNT ?? "0", 10);
|
|
862
|
+
let index = Number.isInteger(existing) && existing > 0 ? existing : 0;
|
|
863
|
+
const out = { ...env };
|
|
864
|
+
for (const [key, value] of entries) {
|
|
865
|
+
out[`GIT_CONFIG_KEY_${index}`] = key;
|
|
866
|
+
out[`GIT_CONFIG_VALUE_${index}`] = value;
|
|
867
|
+
index += 1;
|
|
868
|
+
}
|
|
869
|
+
out.GIT_CONFIG_COUNT = String(index);
|
|
870
|
+
return out;
|
|
871
|
+
}
|
|
872
|
+
function deliver(version, entries, env) {
|
|
873
|
+
if (atLeast(version, ENV_CONFIG_SINCE)) return { args: [], env: withEnvConfig(env, entries) };
|
|
874
|
+
return { args: entries.flatMap(([key, value]) => ["-c", `${key}=${value}`]), env };
|
|
875
|
+
}
|
|
876
|
+
function subcommandFlags(command) {
|
|
877
|
+
const flags = [];
|
|
878
|
+
if (DIFF_COMMANDS.has(command)) flags.push("--no-ext-diff", "--no-textconv");
|
|
879
|
+
if (command === "status") flags.push("--ignore-submodules=all");
|
|
880
|
+
return flags;
|
|
881
|
+
}
|
|
882
|
+
function parseFilterNames(stdout, scoped) {
|
|
883
|
+
const fields = stdout.split("\0");
|
|
884
|
+
const names = /* @__PURE__ */ new Set();
|
|
885
|
+
for (let i = 0; i < fields.length; i += scoped ? 2 : 1) {
|
|
886
|
+
const scope = scoped ? fields[i] : "";
|
|
887
|
+
const key = scoped ? fields[i + 1] : fields[i];
|
|
888
|
+
if (!key || scoped && TRUSTED_SCOPES.has(scope)) continue;
|
|
889
|
+
const match = /^filter\.([\s\S]+)\.[^.]+$/i.exec(key);
|
|
890
|
+
if (match) names.add(match[1]);
|
|
891
|
+
}
|
|
892
|
+
return [...names];
|
|
893
|
+
}
|
|
894
|
+
async function untrustedFilterNames(cwd, version) {
|
|
895
|
+
const scoped = atLeast(version, SHOW_SCOPE_SINCE);
|
|
896
|
+
const hardened = deliver(version, HARDENING_CONFIG, baseEnv(true));
|
|
897
|
+
const args = [...hardened.args, "config", "-z", ...scoped ? ["--show-scope"] : [], "--name-only", "--get-regexp", "^filter\\."];
|
|
898
|
+
let stdout;
|
|
899
|
+
try {
|
|
900
|
+
stdout = await configLister(cwd, args, hardened.env);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (error.code !== 1) throw error;
|
|
903
|
+
stdout = "";
|
|
904
|
+
}
|
|
905
|
+
return parseFilterNames(stdout, scoped);
|
|
906
|
+
}
|
|
907
|
+
function comparablePath(path2, platform = process.platform) {
|
|
908
|
+
const unified = path2.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
909
|
+
return platform === "win32" ? unified.toLowerCase() : unified;
|
|
910
|
+
}
|
|
911
|
+
function samePath(a, b, platform = process.platform) {
|
|
912
|
+
return comparablePath(a, platform) === comparablePath(b, platform);
|
|
913
|
+
}
|
|
914
|
+
async function discoveredRoot(cwd) {
|
|
915
|
+
let dir = await (0, import_promises.realpath)(cwd);
|
|
916
|
+
for (; ; ) {
|
|
917
|
+
try {
|
|
918
|
+
await (0, import_promises.stat)((0, import_node_path.join)(dir, ".git"));
|
|
919
|
+
return dir;
|
|
920
|
+
} catch {
|
|
921
|
+
const parent = (0, import_node_path.dirname)(dir);
|
|
922
|
+
if (parent === dir) return void 0;
|
|
923
|
+
dir = parent;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function assertWorkTreeIsHere(cwd, command, version) {
|
|
928
|
+
const root = await discoveredRoot(cwd);
|
|
929
|
+
if (!root) throw new SafeGitRefusal(`"git ${command}" was not run: no .git was found at or above ${cwd}.`);
|
|
930
|
+
const hardened = deliver(version, HARDENING_CONFIG, baseEnv(true));
|
|
931
|
+
const { stdout } = await execFileAsync("git", [...hardened.args, "rev-parse", "--show-toplevel"], {
|
|
932
|
+
cwd,
|
|
933
|
+
env: hardened.env,
|
|
934
|
+
timeout: QUERY_TIMEOUT_MS,
|
|
935
|
+
windowsHide: true,
|
|
936
|
+
encoding: "utf8"
|
|
937
|
+
});
|
|
938
|
+
const reported = stdout.trim();
|
|
939
|
+
const top = reported ? await (0, import_promises.realpath)(reported).catch(() => reported) : "";
|
|
940
|
+
if (!samePath(top, root)) {
|
|
941
|
+
throw new SafeGitRefusal(
|
|
942
|
+
`"git ${command}" was not run: this repo's git settings put its working tree at ${top || "an unknown folder"}, not ${root}.`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return root;
|
|
946
|
+
}
|
|
947
|
+
async function realOrResolved(path2) {
|
|
948
|
+
return (0, import_promises.realpath)(path2).catch(() => path2);
|
|
949
|
+
}
|
|
950
|
+
async function assertRepositoryBelongsHere(root, command) {
|
|
951
|
+
const dotGit = (0, import_node_path.join)(root, ".git");
|
|
952
|
+
const entry = await (0, import_promises.lstat)(dotGit);
|
|
953
|
+
if (entry.isDirectory()) return;
|
|
954
|
+
let gitDir;
|
|
955
|
+
if (entry.isSymbolicLink()) {
|
|
956
|
+
gitDir = await realOrResolved(dotGit);
|
|
957
|
+
} else {
|
|
958
|
+
const pointer = /^gitdir:\s*(.+?)\s*$/m.exec(await (0, import_promises.readFile)(dotGit, "utf8"));
|
|
959
|
+
if (!pointer) throw new SafeGitRefusal(`"git ${command}" was not run: ${dotGit} is a file without a gitdir line.`);
|
|
960
|
+
gitDir = await realOrResolved((0, import_node_path.resolve)(root, pointer[1]));
|
|
961
|
+
}
|
|
962
|
+
if (comparablePath(gitDir).includes("/.git/modules/")) return;
|
|
963
|
+
try {
|
|
964
|
+
await (0, import_promises.stat)((0, import_node_path.join)(gitDir, "commondir"));
|
|
965
|
+
const back = (await (0, import_promises.readFile)((0, import_node_path.join)(gitDir, "gitdir"), "utf8")).trim();
|
|
966
|
+
if (samePath(await realOrResolved((0, import_node_path.resolve)(gitDir, back)), await realOrResolved(dotGit))) return;
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
throw new SafeGitRefusal(
|
|
970
|
+
`"git ${command}" was not run: .git here points to ${gitDir}, which is not a linked worktree or submodule of this folder, so it would change that other repository.`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
function isTreeToTreeDiff(rest) {
|
|
974
|
+
const end = rest.indexOf("--");
|
|
975
|
+
const before = end === -1 ? rest : rest.slice(0, end);
|
|
976
|
+
if (before.some((arg) => arg.startsWith("-") && !TREE_DIFF_OPTIONS.has(arg))) return false;
|
|
977
|
+
const revisions = before.filter((arg) => !arg.startsWith("-"));
|
|
978
|
+
return revisions.length === 2 || revisions.length === 1 && revisions[0].includes("..");
|
|
979
|
+
}
|
|
980
|
+
function readsWorkingTree(command, rest) {
|
|
981
|
+
if (command === "diff") return !isTreeToTreeDiff(rest);
|
|
982
|
+
return WORKTREE_COMMANDS.has(command);
|
|
983
|
+
}
|
|
984
|
+
function asRefusal(error, command) {
|
|
985
|
+
if (error instanceof SafeGitRefusal) return error;
|
|
986
|
+
const detail = (error instanceof Error ? error.message : String(error)).split(/\r?\n/).find((line) => line.trim()) ?? "unknown error";
|
|
987
|
+
return new SafeGitRefusal(`"git ${command}" was not run: git could not check this repo's settings (${detail.trim()}).`, { cause: error });
|
|
988
|
+
}
|
|
989
|
+
function filterOverrides(names) {
|
|
990
|
+
return names.flatMap((name) => [
|
|
991
|
+
[`filter.${name}.clean`, ""],
|
|
992
|
+
[`filter.${name}.smudge`, ""],
|
|
993
|
+
[`filter.${name}.process`, ""],
|
|
994
|
+
[`filter.${name}.required`, "false"]
|
|
995
|
+
]);
|
|
996
|
+
}
|
|
997
|
+
async function safeGitInvocation(cwd, args) {
|
|
998
|
+
const [command = "", ...rest] = args;
|
|
999
|
+
const version = await gitVersion();
|
|
1000
|
+
const touchesWorktree = readsWorkingTree(command, rest);
|
|
1001
|
+
let entries = HARDENING_CONFIG;
|
|
1002
|
+
if (touchesWorktree) {
|
|
1003
|
+
let names;
|
|
1004
|
+
try {
|
|
1005
|
+
const root = await assertWorkTreeIsHere(cwd, command, version);
|
|
1006
|
+
if (WRITE_COMMANDS.has(command)) await assertRepositoryBelongsHere(root, command);
|
|
1007
|
+
names = await untrustedFilterNames(cwd, version);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
throw asRefusal(error, command);
|
|
1010
|
+
}
|
|
1011
|
+
if (!atLeast(version, ENV_CONFIG_SINCE)) {
|
|
1012
|
+
const unsafe = names.find((name) => /[=\u0000-\u001f\u007f]/.test(name));
|
|
1013
|
+
if (unsafe !== void 0) {
|
|
1014
|
+
throw new SafeGitRefusal(
|
|
1015
|
+
`"git ${command}" was not run: this repo defines a git filter named ${JSON.stringify(unsafe)}, which git ${version.join(".")} cannot be told to switch off. Update git to 2.31 or newer.`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
entries = [...HARDENING_CONFIG, ...filterOverrides(names)];
|
|
1020
|
+
}
|
|
1021
|
+
const delivered = deliver(version, entries, baseEnv(touchesWorktree));
|
|
1022
|
+
return {
|
|
1023
|
+
args: [...delivered.args, command, ...subcommandFlags(command), ...rest],
|
|
1024
|
+
env: delivered.env
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/databaseEvidence.ts
|
|
1029
|
+
var DATABASE_CODE_PATTERNS = {
|
|
1030
|
+
js: /['"](?:@supabase\/[\w-]+|pg|postgres|slonik|@electric-sql\/pglite|@neondatabase\/serverless|@vercel\/postgres|@prisma\/client|drizzle-orm(?:\/[\w-]+)?|mongoose|mongodb)['"]|@supabase\/supabase-js|\bsupabase\.createClient\(/,
|
|
1031
|
+
deno: /deno\.land\/x\/postgres|\bnpm:(?:postgres|pg)\b|\bjsr:@supabase\//,
|
|
1032
|
+
dart: /\bpackage:(?:supabase_flutter|supabase|postgres)\//,
|
|
1033
|
+
python: /^\s*(?:from|import)\s+(?:supabase|psycopg2?|sqlalchemy|sqlmodel|asyncpg|alembic)\b/m,
|
|
1034
|
+
go: /github\.com\/(?:lib\/pq|jackc\/pgx|go-sql-driver\/mysql)/,
|
|
1035
|
+
php: /['"](?:pgsql|mysql):(?:host|dbname|unix_socket)=/,
|
|
1036
|
+
connectionUrl: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\//,
|
|
1037
|
+
envName: /\b[A-Z_]*SUPABASE_[A-Z_]+\b|\b[A-Z_]*(?:DATABASE|DB|POSTGRES|NEON)_(?:URL|URI|HOST)\b|\bMONGODB_URI\b/
|
|
1038
|
+
};
|
|
1039
|
+
var DATABASE_CODE_PATTERN_LIST = Object.values(DATABASE_CODE_PATTERNS);
|
|
1040
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
1041
|
+
|
|
1042
|
+
// src/localStation.ts
|
|
1043
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
1044
|
+
var ENV_NAME = "[A-Z][A-Z0-9_]{2,}";
|
|
1045
|
+
var ENV_REFERENCES = [
|
|
1046
|
+
new RegExp(`process\\.env\\??\\.(${ENV_NAME})\\b`, "g"),
|
|
1047
|
+
new RegExp(`process\\.env\\??\\.?\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g"),
|
|
1048
|
+
new RegExp(`import\\.meta\\.env\\??\\.(${ENV_NAME})\\b`, "g"),
|
|
1049
|
+
new RegExp(`import\\.meta\\.env\\??\\.?\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g"),
|
|
1050
|
+
new RegExp(`Deno\\.env\\.get\\(\\s*['"\`](${ENV_NAME})['"\`]\\s*\\)`, "g"),
|
|
1051
|
+
new RegExp(`Bun\\.env\\.(${ENV_NAME})\\b`, "g"),
|
|
1052
|
+
// Hono and Cloudflare Workers bindings: c.env.X
|
|
1053
|
+
new RegExp(`\\b(?:c|ctx|context)\\.env\\.(${ENV_NAME})\\b`, "g")
|
|
1054
|
+
];
|
|
1055
|
+
|
|
1056
|
+
// src/commands/checkChange.ts
|
|
1057
|
+
var execFileAsync4 = (0, import_node_util4.promisify)(import_node_child_process4.execFile);
|
|
1058
|
+
var MAX_BLOB_BYTES = SOURCE_LIMITS.fileBytes;
|
|
1059
|
+
var MAX_SNAPSHOT_BYTES = 128e6;
|
|
1060
|
+
var SKIP_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".turbo", ".vercel", ".output", "dist", "build", "coverage"]);
|
|
1061
|
+
async function git(cwd, args) {
|
|
1062
|
+
const call = await safeGitInvocation(cwd, args);
|
|
1063
|
+
const { stdout } = await execFileAsync4("git", call.args, { cwd, env: call.env, windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
|
|
1064
|
+
return stdout;
|
|
1065
|
+
}
|
|
1066
|
+
function parseTree(output2) {
|
|
1067
|
+
const entries = [];
|
|
1068
|
+
for (const record2 of output2.split("\0")) {
|
|
1069
|
+
if (!record2) continue;
|
|
1070
|
+
const tab = record2.indexOf(" ");
|
|
1071
|
+
const [mode, type, sha2, size] = record2.slice(0, tab).trim().split(/\s+/);
|
|
1072
|
+
entries.push({ mode, type, sha: sha2, size: Number.parseInt(size, 10) || 0, path: record2.slice(tab + 1) });
|
|
1073
|
+
}
|
|
1074
|
+
return entries;
|
|
1075
|
+
}
|
|
1076
|
+
function wanted(entry) {
|
|
1077
|
+
if (entry.type !== "blob" || entry.mode !== "100644" && entry.mode !== "100755") return false;
|
|
1078
|
+
return !entry.path.split("/").some((segment) => SKIP_SEGMENTS.has(segment));
|
|
1079
|
+
}
|
|
1080
|
+
async function readBlobs(cwd, shas) {
|
|
1081
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
1082
|
+
if (!shas.length) return blobs;
|
|
1083
|
+
const call = await safeGitInvocation(cwd, ["cat-file", "--batch"]);
|
|
1084
|
+
const output2 = await new Promise((resolveOutput, reject) => {
|
|
1085
|
+
const child = (0, import_node_child_process4.spawn)("git", call.args, { cwd, env: call.env, windowsHide: true });
|
|
1086
|
+
const chunks = [];
|
|
1087
|
+
let stderr = "";
|
|
1088
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
1089
|
+
child.stderr.on("data", (chunk) => {
|
|
1090
|
+
stderr += chunk.toString();
|
|
1091
|
+
});
|
|
1092
|
+
child.on("error", reject);
|
|
1093
|
+
child.on("close", (code) => code === 0 ? resolveOutput(Buffer.concat(chunks)) : reject(new Error(stderr.trim() || `git cat-file exited with ${code}`)));
|
|
1094
|
+
child.stdin.end(`${shas.join("\n")}
|
|
1095
|
+
`);
|
|
1096
|
+
});
|
|
1097
|
+
let offset = 0;
|
|
1098
|
+
while (offset < output2.length) {
|
|
1099
|
+
const newline = output2.indexOf(10, offset);
|
|
1100
|
+
if (newline === -1) break;
|
|
1101
|
+
const [sha2, type, sizeText] = output2.subarray(offset, newline).toString().split(" ");
|
|
1102
|
+
offset = newline + 1;
|
|
1103
|
+
if (type === "missing" || sizeText === void 0) continue;
|
|
1104
|
+
const size = Number.parseInt(sizeText, 10);
|
|
1105
|
+
blobs.set(sha2, output2.subarray(offset, offset + size));
|
|
1106
|
+
offset += size + 1;
|
|
1107
|
+
}
|
|
1108
|
+
return blobs;
|
|
1109
|
+
}
|
|
1110
|
+
async function exportCommit(cwd, commit) {
|
|
1111
|
+
const entries = parseTree(await git(cwd, ["ls-tree", "-r", "-z", "-l", "--full-tree", commit])).filter(wanted);
|
|
1112
|
+
if (entries.some((entry) => entry.size > MAX_BLOB_BYTES) || entries.reduce((sum, entry) => sum + entry.size, 0) > MAX_SNAPSHOT_BYTES) {
|
|
1113
|
+
throw new Error("Git snapshot exceeds the supported size limit (32 MB per file, 128 MB total). The comparison is incomplete.");
|
|
1114
|
+
}
|
|
1115
|
+
const blobs = await readBlobs(cwd, [...new Set(entries.map((entry) => entry.sha))]);
|
|
1116
|
+
const root = await (0, import_promises2.mkdtemp)((0, import_node_path2.join)((0, import_node_os.tmpdir)(), "viberaven-base-"));
|
|
1117
|
+
try {
|
|
1118
|
+
for (const entry of entries) {
|
|
1119
|
+
const target = (0, import_node_path2.resolve)(root, ...entry.path.split("/"));
|
|
1120
|
+
if (!target.startsWith(root + import_node_path2.sep)) throw new Error("Invalid Git snapshot path");
|
|
1121
|
+
const content = blobs.get(entry.sha);
|
|
1122
|
+
if (!content || content.length !== entry.size) throw new Error("Git snapshot blob is missing or truncated");
|
|
1123
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(target), { recursive: true });
|
|
1124
|
+
await (0, import_promises2.writeFile)(target, content);
|
|
1125
|
+
}
|
|
1126
|
+
return root;
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
await (0, import_promises2.rm)(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }).catch(() => void 0);
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/version.ts
|
|
1134
|
+
var VERSION = "1.7.1";
|
|
1135
|
+
|
|
1136
|
+
// src/github-ci/api.ts
|
|
1137
|
+
var import_node_zlib = require("node:zlib");
|
|
1138
|
+
|
|
1139
|
+
// src/github-ci/report.ts
|
|
1140
|
+
var MAX_REPORT_BYTES = 65536;
|
|
1141
|
+
var MARKER = "<!-- viberaven-pr-feedback:v1 -->";
|
|
1142
|
+
var RULE_LABELS = Object.fromEntries([
|
|
1143
|
+
"no_release_tags",
|
|
1144
|
+
"scan_incomplete_service_role",
|
|
1145
|
+
"secrets_in_repo",
|
|
1146
|
+
"rls_disabled",
|
|
1147
|
+
"rls_no_policies",
|
|
1148
|
+
"rls_policy_allows_all_read",
|
|
1149
|
+
"rls_policy_allows_all_write",
|
|
1150
|
+
"security_definer_function_executable",
|
|
1151
|
+
"data_api_grant_without_rls",
|
|
1152
|
+
"data_api_grant_missing",
|
|
1153
|
+
"service_role_key_in_client_env",
|
|
1154
|
+
"service_role_key_in_client_code",
|
|
1155
|
+
"service_role_boundary",
|
|
1156
|
+
"pooler_port_unchecked",
|
|
1157
|
+
"pooler_port_mismatch",
|
|
1158
|
+
"auth_secret_missing",
|
|
1159
|
+
"database_url_missing",
|
|
1160
|
+
"missing_rate_limit",
|
|
1161
|
+
"missing_csp_header",
|
|
1162
|
+
"missing_error_boundary",
|
|
1163
|
+
"missing_health_route",
|
|
1164
|
+
"missing_loading_state",
|
|
1165
|
+
"missing_404_page",
|
|
1166
|
+
"unverified_stripe_webhook",
|
|
1167
|
+
"missing_stripe_webhook",
|
|
1168
|
+
"env_var_drift",
|
|
1169
|
+
"unbounded_query",
|
|
1170
|
+
"missing_image_optimization",
|
|
1171
|
+
"missing_monitoring",
|
|
1172
|
+
"openai_key_in_browser"
|
|
1173
|
+
].map((id) => [id, id === "rls_disabled" ? "Row level security disabled" : id.replace(/_/g, " ")]));
|
|
1174
|
+
var sha = (value) => typeof value === "string" && /^[a-f0-9]{40}$/.test(value);
|
|
1175
|
+
var positive = (value) => Number.isSafeInteger(value) && value > 0;
|
|
1176
|
+
var repositoryName = (value) => typeof value === "string" && /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/.test(value);
|
|
1177
|
+
function record(value) {
|
|
1178
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected object");
|
|
1179
|
+
return value;
|
|
1180
|
+
}
|
|
1181
|
+
function exact(value, fields) {
|
|
1182
|
+
if (Object.keys(value).some((k) => !fields.includes(k))) throw new Error("Unexpected field");
|
|
1183
|
+
}
|
|
1184
|
+
function findings(value, strict = false) {
|
|
1185
|
+
if (!Array.isArray(value) || value.length > 100) throw new Error("Invalid findings");
|
|
1186
|
+
return value.map((item) => {
|
|
1187
|
+
const f = record(item);
|
|
1188
|
+
if (strict) exact(f, ["id", "severity"]);
|
|
1189
|
+
if (!Object.hasOwn(RULE_LABELS, f.id) || !["critical", "warning", "info"].includes(f.severity)) throw new Error("Unknown finding");
|
|
1190
|
+
return { id: f.id, severity: f.severity };
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function checkIdentity(i) {
|
|
1194
|
+
if (!repositoryName(i.repository) || !repositoryName(i.headRepository) || !positive(i.repositoryId) || !positive(i.pr) || !sha(i.head) || !sha(i.base) || !positive(i.runId) || !positive(i.attempt) || i.version !== VERSION || i.validationRunId !== void 0 && !positive(i.validationRunId) || i.validationAttempt !== void 0 && !positive(i.validationAttempt)) throw new Error("Invalid identity");
|
|
1195
|
+
}
|
|
1196
|
+
function adaptCheckResult(identity, stdout, exitCode, durationMs) {
|
|
1197
|
+
checkIdentity(identity);
|
|
1198
|
+
const report = {
|
|
1199
|
+
...identity,
|
|
1200
|
+
schema: 1,
|
|
1201
|
+
status: "incomplete",
|
|
1202
|
+
durationMs: Math.min(36e5, Math.max(0, Math.round(durationMs))),
|
|
1203
|
+
mergeBase: null,
|
|
1204
|
+
introduced: [],
|
|
1205
|
+
resolved: [],
|
|
1206
|
+
unchanged: [],
|
|
1207
|
+
usageKey: `${identity.repositoryId}:${identity.pr}:${identity.head}`,
|
|
1208
|
+
executionKey: `${identity.runId}:${identity.attempt}:${identity.validationRunId ?? identity.runId}:${identity.validationAttempt ?? identity.attempt}`
|
|
1209
|
+
};
|
|
1210
|
+
try {
|
|
1211
|
+
if (Buffer.byteLength(stdout) > 4e6 || exitCode !== 0 && exitCode !== 1) return report;
|
|
1212
|
+
const change = record(record(JSON.parse(stdout)).change);
|
|
1213
|
+
if (change.base !== identity.base || !sha(change.baseCommit)) return report;
|
|
1214
|
+
const detail = record(change.findings);
|
|
1215
|
+
const introduced = findings(detail.introduced), resolved = findings(detail.resolved), unchanged = findings(detail.unchanged);
|
|
1216
|
+
for (const [key, list] of Object.entries({ introduced, resolved, unchanged })) {
|
|
1217
|
+
if (JSON.stringify(change[key]) !== JSON.stringify(list.map((f) => f.id))) return report;
|
|
1218
|
+
}
|
|
1219
|
+
if ((introduced.some((f) => f.severity === "critical") ? 1 : 0) !== exitCode) return report;
|
|
1220
|
+
if ([...introduced, ...resolved, ...unchanged].some((f) => f.id.startsWith("scan_incomplete_"))) return report;
|
|
1221
|
+
return { ...report, status: "complete", mergeBase: change.baseCommit, introduced, resolved, unchanged };
|
|
1222
|
+
} catch {
|
|
1223
|
+
return report;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function validateReport(raw) {
|
|
1227
|
+
if (Buffer.byteLength(raw) > MAX_REPORT_BYTES) throw new Error("Report too large");
|
|
1228
|
+
const r = record(JSON.parse(raw));
|
|
1229
|
+
exact(r, ["repository", "repositoryId", "pr", "headRepository", "head", "base", "runId", "attempt", "version", "validationRunId", "validationAttempt", "schema", "status", "durationMs", "mergeBase", "introduced", "resolved", "unchanged", "usageKey", "executionKey"]);
|
|
1230
|
+
checkIdentity(r);
|
|
1231
|
+
if (r.schema !== 1 || !["complete", "incomplete"].includes(r.status) || !Number.isInteger(r.durationMs) || r.durationMs < 0 || r.durationMs > 36e5) throw new Error("Invalid report");
|
|
1232
|
+
for (const k of ["introduced", "resolved", "unchanged"]) findings(r[k], true);
|
|
1233
|
+
if (r.status === "complete" ? !sha(r.mergeBase) : r.mergeBase !== null || r.introduced.length || r.resolved.length || r.unchanged.length) throw new Error("Invalid completion");
|
|
1234
|
+
if (r.status === "complete" && [...r.introduced, ...r.resolved, ...r.unchanged].some((f) => f.id.startsWith("scan_incomplete_"))) throw new Error("Incomplete comparison evidence");
|
|
1235
|
+
const expected = adaptCheckResult(r, "", null, r.durationMs);
|
|
1236
|
+
if (r.usageKey !== expected.usageKey || r.executionKey !== expected.executionKey) throw new Error("Invalid measurement keys");
|
|
1237
|
+
return r;
|
|
1238
|
+
}
|
|
1239
|
+
function renderComment(report) {
|
|
1240
|
+
const r = validateReport(JSON.stringify(report));
|
|
1241
|
+
const blockers = r.introduced.filter((f) => f.severity === "critical").length;
|
|
1242
|
+
const lines = [
|
|
1243
|
+
MARKER,
|
|
1244
|
+
`<!-- viberaven-run:${r.runId}:${r.attempt}:${r.validationRunId ?? r.runId}:${r.validationAttempt ?? r.attempt} -->`,
|
|
1245
|
+
`### VibeRaven: ${r.status === "incomplete" ? "Check incomplete" : blockers ? `${blockers} new blocker(s)` : r.introduced.length ? `${r.introduced.length} new finding(s)` : "No new findings"}`,
|
|
1246
|
+
"",
|
|
1247
|
+
`Head: \`${r.head}\`
|
|
1248
|
+
Base: \`${r.base}\``,
|
|
1249
|
+
""
|
|
1250
|
+
];
|
|
1251
|
+
if (r.status === "complete") {
|
|
1252
|
+
lines.push(`Compared with merge base \`${r.mergeBase}\`.`, "");
|
|
1253
|
+
for (const [label, list] of [["Introduced", r.introduced], ["Resolved", r.resolved]]) {
|
|
1254
|
+
if (list.length) lines.push(`**${label}:**`, "", ...list.map((f) => `- ${f.severity}: ${RULE_LABELS[f.id]} (\`${f.id}\`)`), "");
|
|
1255
|
+
}
|
|
1256
|
+
lines.push(`${r.unchanged.length} finding(s) already present.`, "");
|
|
1257
|
+
} else lines.push("The comparison did not finish or its evidence could not be validated. No clean verdict is available.", "");
|
|
1258
|
+
lines.push(
|
|
1259
|
+
`[Source run](https://github.com/${r.repository}/actions/runs/${r.runId}) \xB7 [Trusted recheck](https://github.com/${r.repository}/actions/runs/${r.validationRunId ?? r.runId}) \xB7 CLI ${r.version} \xB7 ${r.durationMs} ms`,
|
|
1260
|
+
"",
|
|
1261
|
+
"Repository evidence only. This does not verify live provider settings and is not a security audit. Bounded finding metadata is retained in GitHub Actions for one day."
|
|
1262
|
+
);
|
|
1263
|
+
return lines.join("\n") + "\n";
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/github-ci/api.ts
|
|
1267
|
+
var MAX_ARCHIVE_BYTES = 1e5;
|
|
1268
|
+
function crc32(data) {
|
|
1269
|
+
let crc = 4294967295;
|
|
1270
|
+
for (const byte of data) {
|
|
1271
|
+
crc ^= byte;
|
|
1272
|
+
for (let i = 0; i < 8; i++) crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
|
|
1273
|
+
}
|
|
1274
|
+
return (crc ^ 4294967295) >>> 0;
|
|
1275
|
+
}
|
|
1276
|
+
function readReportZip(zip) {
|
|
1277
|
+
if (zip.length > MAX_ARCHIVE_BYTES || zip.length < 100) throw new Error("Invalid archive size");
|
|
1278
|
+
const end = zip.length - 22;
|
|
1279
|
+
if (zip.readUInt32LE(end) !== 101010256 || zip.readUInt16LE(end + 4) !== 0 || zip.readUInt16LE(end + 6) !== 0 || zip.readUInt16LE(end + 8) !== 1 || zip.readUInt16LE(end + 10) !== 1 || zip.readUInt16LE(end + 20) !== 0) throw new Error("Archive must contain exactly one file");
|
|
1280
|
+
const central = zip.readUInt32LE(end + 16), centralSize = zip.readUInt32LE(end + 12);
|
|
1281
|
+
if (central + centralSize !== end || centralSize < 46 || central > end - 46 || zip.readUInt32LE(central) !== 33639248) throw new Error("Invalid directory");
|
|
1282
|
+
const flags = zip.readUInt16LE(central + 8), method = zip.readUInt16LE(central + 10);
|
|
1283
|
+
const compressed = zip.readUInt32LE(central + 20), size = zip.readUInt32LE(central + 24);
|
|
1284
|
+
const nameLength = zip.readUInt16LE(central + 28), extraLength = zip.readUInt16LE(central + 30), commentLength = zip.readUInt16LE(central + 32);
|
|
1285
|
+
const mode = zip.readUInt32LE(central + 38) >>> 16 & 61440;
|
|
1286
|
+
if ((flags & ~(8 | 2048)) !== 0 || ![0, 8].includes(method) || !size || size > MAX_REPORT_BYTES || compressed > MAX_ARCHIVE_BYTES || ![0, 32768].includes(mode) || zip.readUInt16LE(central + 34) !== 0 || zip.readUInt32LE(central + 42) !== 0 || centralSize !== 46 + nameLength + extraLength + commentLength || zip.subarray(central + 46, central + 46 + nameLength).toString("utf8") !== "report.json") throw new Error("Unsafe report entry");
|
|
1287
|
+
if (zip.readUInt32LE(0) !== 67324752 || zip.readUInt16LE(6) !== flags || zip.readUInt16LE(8) !== method) throw new Error("Invalid local entry");
|
|
1288
|
+
const localName = zip.readUInt16LE(26), localExtra = zip.readUInt16LE(28), start = 30 + localName + localExtra;
|
|
1289
|
+
if (zip.subarray(30, 30 + localName).toString("utf8") !== "report.json" || start + compressed > central) throw new Error("Invalid local file");
|
|
1290
|
+
const tail = central - start - compressed;
|
|
1291
|
+
if (flags & 8 ? ![12, 16].includes(tail) : tail !== 0) throw new Error("Unexpected archive entries");
|
|
1292
|
+
const expectedCrc = zip.readUInt32LE(central + 16);
|
|
1293
|
+
if (!(flags & 8) && (zip.readUInt32LE(14) !== expectedCrc || zip.readUInt32LE(18) !== compressed || zip.readUInt32LE(22) !== size)) throw new Error("Conflicting local metadata");
|
|
1294
|
+
if (flags & 8) {
|
|
1295
|
+
const descriptor = start + compressed + (tail === 16 ? 4 : 0);
|
|
1296
|
+
if (tail === 16 && zip.readUInt32LE(start + compressed) !== 134695760 || zip.readUInt32LE(descriptor) !== expectedCrc || zip.readUInt32LE(descriptor + 4) !== compressed || zip.readUInt32LE(descriptor + 8) !== size) throw new Error("Invalid data descriptor");
|
|
1297
|
+
}
|
|
1298
|
+
const data = method === 8 ? (0, import_node_zlib.inflateRawSync)(zip.subarray(start, start + compressed), { maxOutputLength: MAX_REPORT_BYTES }) : zip.subarray(start, start + compressed);
|
|
1299
|
+
if (data.length !== size || crc32(data) !== expectedCrc) throw new Error("Corrupt report");
|
|
1300
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
1301
|
+
}
|
|
1302
|
+
async function bounded(response, limit) {
|
|
1303
|
+
if (Number(response.headers.get("content-length")) > limit || !response.body) throw new Error("Response too large or missing");
|
|
1304
|
+
const reader = response.body.getReader(), chunks = [];
|
|
1305
|
+
let size = 0;
|
|
1306
|
+
try {
|
|
1307
|
+
while (true) {
|
|
1308
|
+
const next = await reader.read();
|
|
1309
|
+
if (next.done) break;
|
|
1310
|
+
size += next.value.length;
|
|
1311
|
+
if (size > limit) throw new Error("Response too large");
|
|
1312
|
+
chunks.push(next.value);
|
|
1313
|
+
}
|
|
1314
|
+
} finally {
|
|
1315
|
+
await reader.cancel().catch(() => void 0);
|
|
1316
|
+
}
|
|
1317
|
+
return Buffer.concat(chunks);
|
|
1318
|
+
}
|
|
1319
|
+
function githubApi(token, fetcher = fetch) {
|
|
1320
|
+
if (!token || /[\r\n]/.test(token)) throw new Error("Missing GitHub token");
|
|
1321
|
+
async function request(path2, method = "GET", body, redirect = "error") {
|
|
1322
|
+
if (!/^\/repos\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\//.test(path2) || path2.includes("..") || path2.includes("#")) throw new Error("Invalid API path");
|
|
1323
|
+
const response = await fetcher(`https://api.github.com${path2}`, {
|
|
1324
|
+
method,
|
|
1325
|
+
redirect,
|
|
1326
|
+
signal: AbortSignal.timeout(3e4),
|
|
1327
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "user-agent": "viberaven-ci", ...body ? { "content-type": "application/json" } : {} },
|
|
1328
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
1329
|
+
});
|
|
1330
|
+
if (!response.ok && !(redirect === "manual" && response.status === 302)) throw new Error(`GitHub API ${response.status}`);
|
|
1331
|
+
return response;
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
async get(path2) {
|
|
1335
|
+
return JSON.parse((await bounded(await request(path2), 2e6)).toString("utf8"));
|
|
1336
|
+
},
|
|
1337
|
+
async write(method, path2, body) {
|
|
1338
|
+
await request(path2, method, body);
|
|
1339
|
+
},
|
|
1340
|
+
async artifact(repository, id) {
|
|
1341
|
+
if (!repositoryName(repository) || !positive(id)) throw new Error("Invalid artifact identity");
|
|
1342
|
+
const redirect = await request(`/repos/${repository}/actions/artifacts/${id}/zip`, "GET", void 0, "manual");
|
|
1343
|
+
if (redirect.status !== 302) throw new Error("Expected GitHub artifact redirect");
|
|
1344
|
+
const location = new URL(redirect.headers.get("location") ?? "");
|
|
1345
|
+
if (location.protocol !== "https:" || location.username || location.password || location.port || !(/\.blob\.core\.windows\.net$/.test(location.hostname) || /\.actions\.githubusercontent\.com$/.test(location.hostname))) throw new Error("Unexpected artifact host");
|
|
1346
|
+
const response = await fetcher(location, { redirect: "error", signal: AbortSignal.timeout(3e4) });
|
|
1347
|
+
if (!response.ok) throw new Error("Artifact download failed");
|
|
1348
|
+
return readReportZip(await bounded(response, MAX_ARCHIVE_BYTES));
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/commands/initGithub.ts
|
|
1354
|
+
var import_node_child_process5 = require("node:child_process");
|
|
1355
|
+
var import_node_path3 = require("node:path");
|
|
1356
|
+
var import_node_util5 = require("node:util");
|
|
1357
|
+
var execFileAsync5 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
|
|
1358
|
+
var WORKFLOW_PATH = (0, import_node_path3.join)(".github", "workflows", "viberaven.yml");
|
|
1359
|
+
var PUBLISHER_PATH = (0, import_node_path3.join)(".github", "workflows", "viberaven-comment.yml");
|
|
1360
|
+
var SCAN_NAME = "VibeRaven";
|
|
1361
|
+
var PUBLISHER_NAME = "VibeRaven feedback";
|
|
1362
|
+
var NODE = "949feb2413d6458794dcd2491c4babbbce0c15c1";
|
|
1363
|
+
var UPLOAD = "cf430e030ddbb5b0abf93d22962f4752f3646cd9";
|
|
1364
|
+
function install(version) {
|
|
1365
|
+
if (!/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(version)) throw new Error("Invalid package version");
|
|
1366
|
+
return [
|
|
1367
|
+
` - uses: actions/setup-node@${NODE} # v7.1.0`,
|
|
1368
|
+
" with:",
|
|
1369
|
+
" node-version: 24",
|
|
1370
|
+
" package-manager-cache: false",
|
|
1371
|
+
" - name: Install pinned VibeRaven outside the repository",
|
|
1372
|
+
" working-directory: ${{ runner.temp }}",
|
|
1373
|
+
" run: |",
|
|
1374
|
+
' tool_dir=$(mktemp -d "$RUNNER_TEMP/viberaven-tool.XXXXXX")',
|
|
1375
|
+
' cd "$tool_dir"',
|
|
1376
|
+
" export NPM_CONFIG_USERCONFIG=/dev/null NPM_CONFIG_GLOBALCONFIG=/dev/null",
|
|
1377
|
+
` npm install --ignore-scripts --no-audit --no-fund --package-lock=false --registry=https://registry.npmjs.org @viberaven/cli@${version}`,
|
|
1378
|
+
' echo "VR_HELPER=$tool_dir/node_modules/@viberaven/cli/dist/github-ci.js" >> "$GITHUB_ENV"'
|
|
1379
|
+
];
|
|
1380
|
+
}
|
|
1381
|
+
function artifact(name) {
|
|
1382
|
+
return [
|
|
1383
|
+
` - uses: actions/upload-artifact@${UPLOAD} # v7.0.2`,
|
|
1384
|
+
" if: always()",
|
|
1385
|
+
" with:",
|
|
1386
|
+
` name: ${name}`,
|
|
1387
|
+
" path: ${{ runner.temp }}/viberaven-report/report.json",
|
|
1388
|
+
" if-no-files-found: error",
|
|
1389
|
+
" retention-days: 1",
|
|
1390
|
+
" archive: true"
|
|
1391
|
+
];
|
|
1392
|
+
}
|
|
1393
|
+
function renderPublisherWorkflow(version = VERSION) {
|
|
1394
|
+
return [
|
|
1395
|
+
"# This workflow runs from the default branch. It never trusts the PR workflow artifact.",
|
|
1396
|
+
"# Recheck uses a read-only token and exports regular Git blobs without running repository scripts.",
|
|
1397
|
+
`name: ${PUBLISHER_NAME}`,
|
|
1398
|
+
"on:",
|
|
1399
|
+
" workflow_run:",
|
|
1400
|
+
` workflows: [${SCAN_NAME}]`,
|
|
1401
|
+
" types: [completed]",
|
|
1402
|
+
"permissions: {}",
|
|
1403
|
+
"concurrency:",
|
|
1404
|
+
" group: viberaven-feedback-${{ github.repository_id }}-${{ github.event.workflow_run.head_repository.id }}-${{ github.event.workflow_run.head_branch }}",
|
|
1405
|
+
" cancel-in-progress: false",
|
|
1406
|
+
"jobs:",
|
|
1407
|
+
" recheck:",
|
|
1408
|
+
" runs-on: ubuntu-latest",
|
|
1409
|
+
" timeout-minutes: 12",
|
|
1410
|
+
" permissions:",
|
|
1411
|
+
" contents: read",
|
|
1412
|
+
" actions: read",
|
|
1413
|
+
" pull-requests: read",
|
|
1414
|
+
" steps:",
|
|
1415
|
+
...install(version),
|
|
1416
|
+
" - name: Independently check immutable PR commits",
|
|
1417
|
+
" env:",
|
|
1418
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
1419
|
+
" VR_REPORT_DIR: ${{ runner.temp }}/viberaven-report",
|
|
1420
|
+
' run: node "$VR_HELPER" recheck',
|
|
1421
|
+
...artifact("viberaven-validated-${{ github.run_id }}-${{ github.run_attempt }}"),
|
|
1422
|
+
" publish:",
|
|
1423
|
+
" needs: recheck",
|
|
1424
|
+
" if: always()",
|
|
1425
|
+
" runs-on: ubuntu-latest",
|
|
1426
|
+
" timeout-minutes: 5",
|
|
1427
|
+
" permissions:",
|
|
1428
|
+
" contents: read",
|
|
1429
|
+
" actions: read",
|
|
1430
|
+
" pull-requests: write",
|
|
1431
|
+
" steps:",
|
|
1432
|
+
...install(version),
|
|
1433
|
+
" - name: Publish validated feedback",
|
|
1434
|
+
" env:",
|
|
1435
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
1436
|
+
' run: node "$VR_HELPER" publish',
|
|
1437
|
+
""
|
|
1438
|
+
].join("\n");
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/github-ci/publisher.ts
|
|
1442
|
+
var path = (repository) => `/repos/${repository}`;
|
|
1443
|
+
function sameRepo(repo, name, id) {
|
|
1444
|
+
return repo?.full_name === name && positive(repo?.id) && (id === void 0 || repo.id === id);
|
|
1445
|
+
}
|
|
1446
|
+
async function pages(api, url) {
|
|
1447
|
+
const result = [];
|
|
1448
|
+
for (let page = 1; page <= 20; page++) {
|
|
1449
|
+
const data = await api.get(`${url}${url.includes("?") ? "&" : "?"}per_page=100&page=${page}`);
|
|
1450
|
+
if (!Array.isArray(data)) throw new Error("Invalid paginated response");
|
|
1451
|
+
result.push(...data);
|
|
1452
|
+
if (data.length < 100) return result;
|
|
1453
|
+
}
|
|
1454
|
+
throw new Error("Pagination limit reached");
|
|
1455
|
+
}
|
|
1456
|
+
async function resolveSource(api, repository, repositoryId, sourceId) {
|
|
1457
|
+
if (!repositoryName(repository) || !positive(repositoryId) || !positive(sourceId)) throw new Error("Invalid run identity");
|
|
1458
|
+
const run = record(await api.get(`${path(repository)}/actions/runs/${sourceId}`));
|
|
1459
|
+
if (run.id !== sourceId || run.status !== "completed" || run.event !== "pull_request" || run.path !== ".github/workflows/viberaven.yml" || !sameRepo(run.repository, repository, repositoryId) || !repositoryName(run.head_repository?.full_name) || !positive(run.head_repository?.id) || !sha(run.head_sha) || !positive(run.run_attempt) || !positive(run.workflow_id) || typeof run.head_branch !== "string") throw new Error("Untrusted source run");
|
|
1460
|
+
const workflow = await api.get(`${path(repository)}/actions/workflows/${run.workflow_id}`);
|
|
1461
|
+
if (workflow.id !== run.workflow_id || workflow.path !== run.path) throw new Error("Unexpected source workflow");
|
|
1462
|
+
let candidates = Array.isArray(run.pull_requests) ? run.pull_requests : [];
|
|
1463
|
+
if (!candidates.length) candidates = await pages(api, `${path(repository)}/commits/${run.head_sha}/pulls`);
|
|
1464
|
+
if (!candidates.length) {
|
|
1465
|
+
const owner = run.head_repository.full_name.split("/")[0];
|
|
1466
|
+
candidates = await pages(api, `${path(repository)}/pulls?state=open&head=${encodeURIComponent(`${owner}:${run.head_branch}`)}`);
|
|
1467
|
+
}
|
|
1468
|
+
const matches = [];
|
|
1469
|
+
for (const number of [...new Set(candidates.map((p) => p.number))]) {
|
|
1470
|
+
if (!positive(number)) throw new Error("Invalid PR association");
|
|
1471
|
+
const pr2 = await api.get(`${path(repository)}/pulls/${number}`);
|
|
1472
|
+
if (pr2.number === number && pr2.state === "open" && pr2.head?.sha === run.head_sha && pr2.head?.ref === run.head_branch && sameRepo(pr2.head?.repo, run.head_repository.full_name, run.head_repository.id) && sameRepo(pr2.base?.repo, repository, repositoryId) && sha(pr2.base?.sha)) matches.push(pr2);
|
|
1473
|
+
}
|
|
1474
|
+
if (matches.length !== 1) throw new Error("No unique current open PR for source run");
|
|
1475
|
+
const pr = matches[0];
|
|
1476
|
+
return { pr, identity: {
|
|
1477
|
+
repository,
|
|
1478
|
+
repositoryId,
|
|
1479
|
+
pr: pr.number,
|
|
1480
|
+
headRepository: pr.head.repo.full_name,
|
|
1481
|
+
head: pr.head.sha,
|
|
1482
|
+
base: pr.base.sha,
|
|
1483
|
+
runId: sourceId,
|
|
1484
|
+
attempt: run.run_attempt,
|
|
1485
|
+
version: VERSION
|
|
1486
|
+
} };
|
|
1487
|
+
}
|
|
1488
|
+
async function validateTrustedRun(api, repository, repositoryId, runId, attempt) {
|
|
1489
|
+
if (!positive(runId) || !positive(attempt)) throw new Error("Invalid trusted run identity");
|
|
1490
|
+
const run = await api.get(`${path(repository)}/actions/runs/${runId}`);
|
|
1491
|
+
if (run.id !== runId || run.run_attempt !== attempt || run.event !== "workflow_run" || !sameRepo(run.repository, repository, repositoryId) || run.path !== ".github/workflows/viberaven-comment.yml" || !sha(run.head_sha) || !positive(run.workflow_id)) throw new Error("Invalid trusted workflow");
|
|
1492
|
+
const workflow = await api.get(`${path(repository)}/actions/workflows/${run.workflow_id}`);
|
|
1493
|
+
if (workflow.id !== run.workflow_id || workflow.path !== run.path) throw new Error("Unexpected publisher workflow");
|
|
1494
|
+
const file = await api.get(`${path(repository)}/contents/${run.path}?ref=${run.head_sha}`);
|
|
1495
|
+
if (file.encoding !== "base64" || typeof file.content !== "string" || file.content.length > 4e4 || Buffer.from(file.content, "base64").toString("utf8").replace(/\r\n/g, "\n") !== renderPublisherWorkflow()) throw new Error("Publisher workflow differs from canonical template");
|
|
1496
|
+
}
|
|
1497
|
+
function equalIdentity(report, expected) {
|
|
1498
|
+
for (const [key, value] of Object.entries(expected)) if (report[key] !== value) throw new Error("Report identity mismatch");
|
|
1499
|
+
}
|
|
1500
|
+
function owned(comment) {
|
|
1501
|
+
return positive(comment?.id) && comment.user?.id === 41898282 && comment.user?.login === "github-actions[bot]" && comment.user?.type === "Bot" && typeof comment.body === "string" && comment.body.split("\n")[0] === MARKER;
|
|
1502
|
+
}
|
|
1503
|
+
function later(body, expected) {
|
|
1504
|
+
const match = /^<!-- viberaven-run:(\d+):(\d+):(\d+):(\d+) -->$/m.exec(body);
|
|
1505
|
+
if (!match) return false;
|
|
1506
|
+
const old = match.slice(1).map(Number), now = [expected.runId, expected.attempt, expected.validationRunId, expected.validationAttempt];
|
|
1507
|
+
for (let i = 0; i < old.length; i++) {
|
|
1508
|
+
if (old[i] > now[i]) return true;
|
|
1509
|
+
if (old[i] < now[i]) return false;
|
|
1510
|
+
}
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
async function publish(api, repository, repositoryId, sourceId, trustedId, trustedAttempt) {
|
|
1514
|
+
await validateTrustedRun(api, repository, repositoryId, trustedId, trustedAttempt);
|
|
1515
|
+
const { identity, pr } = await resolveSource(api, repository, repositoryId, sourceId);
|
|
1516
|
+
const expected = { ...identity, validationRunId: trustedId, validationAttempt: trustedAttempt };
|
|
1517
|
+
let report = adaptCheckResult(expected, "", null, 0);
|
|
1518
|
+
try {
|
|
1519
|
+
const artifacts = await api.get(`${path(repository)}/actions/runs/${trustedId}/artifacts?per_page=100&page=1`);
|
|
1520
|
+
if (!Array.isArray(artifacts.artifacts) || artifacts.total_count > 100) throw new Error("Invalid artifact list");
|
|
1521
|
+
const matches = artifacts.artifacts.filter((a) => a.name === `viberaven-validated-${trustedId}-${trustedAttempt}`);
|
|
1522
|
+
if (matches.length !== 1) throw new Error("Missing unique trusted report");
|
|
1523
|
+
const artifact2 = matches[0];
|
|
1524
|
+
if (!positive(artifact2.id) || artifact2.expired !== false || artifact2.workflow_run?.id !== trustedId || !positive(artifact2.size_in_bytes) || artifact2.size_in_bytes > 1e5) throw new Error("Invalid artifact metadata");
|
|
1525
|
+
const candidate = validateReport(await api.artifact(repository, artifact2.id));
|
|
1526
|
+
equalIdentity(candidate, expected);
|
|
1527
|
+
report = candidate;
|
|
1528
|
+
} catch {
|
|
1529
|
+
}
|
|
1530
|
+
const comments = (await pages(api, `${path(repository)}/issues/${pr.number}/comments`)).filter(owned);
|
|
1531
|
+
if (comments.some((c) => later(c.body, expected))) return null;
|
|
1532
|
+
const body = renderComment(report);
|
|
1533
|
+
const current = await api.get(`${path(repository)}/pulls/${pr.number}`);
|
|
1534
|
+
if (current.state !== "open" || current.head?.sha !== identity.head || current.base?.sha !== identity.base || !sameRepo(current.head?.repo, identity.headRepository, pr.head.repo.id) || !sameRepo(current.base?.repo, repository, repositoryId)) throw new Error("PR changed before comment write");
|
|
1535
|
+
const existing = comments.sort((a, b) => a.id - b.id)[0];
|
|
1536
|
+
if (existing) await api.write("PATCH", `${path(repository)}/issues/comments/${existing.id}`, { body });
|
|
1537
|
+
else await api.write("POST", `${path(repository)}/issues/${pr.number}/comments`, { body });
|
|
1538
|
+
return report;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/github-ci/main.ts
|
|
1542
|
+
function scannerEnv(env) {
|
|
1543
|
+
const result = {};
|
|
1544
|
+
for (const key of ["PATH", "Path", "HOME", "USERPROFILE", "SystemRoot", "TEMP", "TMP", "TMPDIR", "LANG"]) if (env[key]) result[key] = env[key];
|
|
1545
|
+
result.GIT_CONFIG_NOSYSTEM = "1";
|
|
1546
|
+
result.GIT_CONFIG_GLOBAL = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
1547
|
+
result.GIT_TERMINAL_PROMPT = "0";
|
|
1548
|
+
return result;
|
|
1549
|
+
}
|
|
1550
|
+
function scan(identity, cwd) {
|
|
1551
|
+
const start = Date.now();
|
|
1552
|
+
const env = scannerEnv(process.env);
|
|
1553
|
+
try {
|
|
1554
|
+
const head = (0, import_node_child_process6.execFileSync)("git", ["-c", "core.hooksPath=/dev/null", "rev-parse", "--verify", "HEAD"], { cwd, env, encoding: "utf8", timeout: 1e4, windowsHide: true }).trim();
|
|
1555
|
+
if (head !== identity.head) throw new Error("Unexpected checked out head");
|
|
1556
|
+
const result = (0, import_node_child_process6.spawnSync)(process.execPath, [(0, import_node_path4.resolve)(__dirname, "cli.js"), "check", "--base", identity.base, "--json"], {
|
|
1557
|
+
cwd,
|
|
1558
|
+
env,
|
|
1559
|
+
encoding: "utf8",
|
|
1560
|
+
timeout: 3e5,
|
|
1561
|
+
maxBuffer: 4e6,
|
|
1562
|
+
windowsHide: true
|
|
1563
|
+
});
|
|
1564
|
+
return adaptCheckResult(identity, result.stdout ?? "", result.error ? null : result.status, Date.now() - start);
|
|
1565
|
+
} catch {
|
|
1566
|
+
return adaptCheckResult(identity, "", null, Date.now() - start);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async function snapshot(identity, token) {
|
|
1570
|
+
const repo = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os2.tmpdir)(), "viberaven-ci-git-"));
|
|
1571
|
+
let tree;
|
|
1572
|
+
const env = scannerEnv(process.env);
|
|
1573
|
+
env.GIT_CONFIG_COUNT = "1";
|
|
1574
|
+
env.GIT_CONFIG_KEY_0 = "http.https://github.com/.extraheader";
|
|
1575
|
+
env.GIT_CONFIG_VALUE_0 = `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
|
|
1576
|
+
const git2 = (...args) => (0, import_node_child_process6.execFileSync)("git", ["-c", "core.hooksPath=/dev/null", "-c", "protocol.file.allow=never", ...args], { cwd: repo, env, timeout: 12e4, maxBuffer: 4e6, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
1577
|
+
try {
|
|
1578
|
+
git2("init", "--quiet", "--template=");
|
|
1579
|
+
git2("fetch", "--no-tags", "--no-recurse-submodules", `https://github.com/${identity.repository}.git`, identity.base);
|
|
1580
|
+
git2("fetch", "--no-tags", "--no-recurse-submodules", `https://github.com/${identity.headRepository}.git`, identity.head);
|
|
1581
|
+
git2("update-ref", "HEAD", identity.head);
|
|
1582
|
+
tree = await exportCommit(repo, identity.head);
|
|
1583
|
+
await (0, import_promises3.rename)((0, import_node_path4.join)(repo, ".git"), (0, import_node_path4.join)(tree, ".git"));
|
|
1584
|
+
return tree;
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
if (tree) await (0, import_promises3.rm)(tree, { recursive: true, force: true });
|
|
1587
|
+
throw error;
|
|
1588
|
+
} finally {
|
|
1589
|
+
await (0, import_promises3.rm)(repo, { recursive: true, force: true });
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function output(report, env) {
|
|
1593
|
+
if (!env.VR_REPORT_DIR) throw new Error("Missing report directory");
|
|
1594
|
+
(0, import_node_fs.mkdirSync)(env.VR_REPORT_DIR, { recursive: true });
|
|
1595
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path4.join)(env.VR_REPORT_DIR, "report.json"), JSON.stringify(report) + "\n", "utf8");
|
|
1596
|
+
if (env.GITHUB_STEP_SUMMARY) (0, import_node_fs.appendFileSync)(env.GITHUB_STEP_SUMMARY, renderComment(report), "utf8");
|
|
1597
|
+
console.log(`VibeRaven: ${report.status}; ${report.introduced.length} introduced, ${report.resolved.length} resolved.`);
|
|
1598
|
+
}
|
|
1599
|
+
async function main(command, env = process.env) {
|
|
1600
|
+
const event = record(JSON.parse((0, import_node_fs.readFileSync)(env.GITHUB_EVENT_PATH ?? "", "utf8")));
|
|
1601
|
+
const repository = env.GITHUB_REPOSITORY ?? "", repositoryId = Number(env.GITHUB_REPOSITORY_ID), runId = Number(env.GITHUB_RUN_ID), attempt = Number(env.GITHUB_RUN_ATTEMPT);
|
|
1602
|
+
if (!repositoryName(repository) || !positive(repositoryId) || !positive(runId) || !positive(attempt)) throw new Error("Invalid GitHub environment");
|
|
1603
|
+
if (command === "scan") {
|
|
1604
|
+
const pr = record(event.pull_request);
|
|
1605
|
+
const identity2 = { repository, repositoryId, pr: pr.number, headRepository: pr.head?.repo?.full_name, head: pr.head?.sha, base: pr.base?.sha, runId, attempt, version: VERSION };
|
|
1606
|
+
const report2 = scan(identity2, process.cwd());
|
|
1607
|
+
output(report2, env);
|
|
1608
|
+
return report2.status === "complete" ? 0 : 2;
|
|
1609
|
+
}
|
|
1610
|
+
const api = githubApi(env.GH_TOKEN ?? "");
|
|
1611
|
+
const sourceId = event.workflow_run?.id;
|
|
1612
|
+
if (!positive(sourceId)) throw new Error("Missing source run");
|
|
1613
|
+
if (command === "publish") {
|
|
1614
|
+
const report2 = await publish(api, repository, repositoryId, sourceId, runId, attempt);
|
|
1615
|
+
if (report2 && env.GITHUB_STEP_SUMMARY) (0, import_node_fs.appendFileSync)(env.GITHUB_STEP_SUMMARY, renderComment(report2), "utf8");
|
|
1616
|
+
return report2?.status === "incomplete" ? 2 : 0;
|
|
1617
|
+
}
|
|
1618
|
+
if (command !== "recheck") throw new Error("Unknown CI command");
|
|
1619
|
+
const { identity } = await resolveSource(api, repository, repositoryId, sourceId);
|
|
1620
|
+
const expected = { ...identity, validationRunId: runId, validationAttempt: attempt };
|
|
1621
|
+
let report = adaptCheckResult(expected, "", null, 0), tree;
|
|
1622
|
+
const started = Date.now();
|
|
1623
|
+
try {
|
|
1624
|
+
tree = await snapshot(expected, env.GH_TOKEN);
|
|
1625
|
+
report = scan(expected, tree);
|
|
1626
|
+
} catch {
|
|
1627
|
+
report.durationMs = Math.min(Date.now() - started, 36e5);
|
|
1628
|
+
} finally {
|
|
1629
|
+
if (tree) await (0, import_promises3.rm)(tree, { recursive: true, force: true });
|
|
1630
|
+
}
|
|
1631
|
+
output(report, env);
|
|
1632
|
+
return report.status === "complete" ? 0 : 2;
|
|
1633
|
+
}
|
|
1634
|
+
if (require.main === module) main(process.argv[2]).then((code) => {
|
|
1635
|
+
process.exitCode = code;
|
|
1636
|
+
}).catch(() => {
|
|
1637
|
+
console.error("VibeRaven CI could not validate this run. No clean verdict was produced.");
|
|
1638
|
+
process.exitCode = 2;
|
|
1639
|
+
});
|
|
1640
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1641
|
+
0 && (module.exports = {
|
|
1642
|
+
main,
|
|
1643
|
+
scan,
|
|
1644
|
+
scannerEnv
|
|
1645
|
+
});
|