next-anteater 0.2.9 → 0.2.11
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/lib/scaffold.mjs +202 -17
- package/package.json +1 -1
package/lib/scaffold.mjs
CHANGED
|
@@ -29,6 +29,143 @@ async function patchRunsRouteDeleteIfMissing(path, isTypeScript) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
async function patchApiRouteMutationGuardIfMissing(path) {
|
|
33
|
+
try {
|
|
34
|
+
const existing = await readFile(path, "utf-8");
|
|
35
|
+
if (existing.includes("Same-origin guard for mutating Anteater requests")) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const guardBlock = [
|
|
40
|
+
" // Same-origin guard for mutating Anteater requests (no app auth integration required)",
|
|
41
|
+
" const requestOrigin = request.nextUrl.origin;",
|
|
42
|
+
' const fetchSite = request.headers.get("sec-fetch-site");',
|
|
43
|
+
' const origin = request.headers.get("origin");',
|
|
44
|
+
' const referer = request.headers.get("referer");',
|
|
45
|
+
" const hasMatchingOrigin = origin === requestOrigin;",
|
|
46
|
+
" const hasMatchingReferer = (() => {",
|
|
47
|
+
" if (!referer) return false;",
|
|
48
|
+
" try {",
|
|
49
|
+
" return new URL(referer).origin === requestOrigin;",
|
|
50
|
+
" } catch {",
|
|
51
|
+
" return false;",
|
|
52
|
+
" }",
|
|
53
|
+
" })();",
|
|
54
|
+
' const hasSameOriginBrowserSignal = fetchSite === "same-origin";',
|
|
55
|
+
" const hasValidSameOriginSignal = hasSameOriginBrowserSignal || hasMatchingOrigin || hasMatchingReferer;",
|
|
56
|
+
" const secret = process.env.ANTEATER_SECRET;",
|
|
57
|
+
' const hasValidSecret = !!secret && request.headers.get("x-anteater-secret") === secret;',
|
|
58
|
+
" if (!hasValidSameOriginSignal && !hasValidSecret) {",
|
|
59
|
+
" return NextResponse.json(",
|
|
60
|
+
' { requestId: "", branch: "", status: "error", error: "Forbidden" },',
|
|
61
|
+
" { status: 403 }",
|
|
62
|
+
" );",
|
|
63
|
+
" }",
|
|
64
|
+
"",
|
|
65
|
+
].join("\n");
|
|
66
|
+
|
|
67
|
+
const contentTypeBlock = [
|
|
68
|
+
' const contentType = request.headers.get("content-type") || "";',
|
|
69
|
+
' if (!contentType.toLowerCase().includes("application/json")) {',
|
|
70
|
+
" return NextResponse.json(",
|
|
71
|
+
' { requestId: "", branch: "", status: "error", error: "Content-Type must be application/json" },',
|
|
72
|
+
" { status: 415 }",
|
|
73
|
+
" );",
|
|
74
|
+
" }",
|
|
75
|
+
"",
|
|
76
|
+
].join("\n");
|
|
77
|
+
|
|
78
|
+
let patched = existing;
|
|
79
|
+
|
|
80
|
+
patched = patched.replace(
|
|
81
|
+
" try {\n const body",
|
|
82
|
+
` try {\n${contentTypeBlock} const body`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const oldAuthPattern = / \/\/ Auth: sec-fetch-site for same-origin \(AnteaterBar\), x-anteater-secret for external[\s\S]*? const repo = getRepo\(\);/;
|
|
86
|
+
if (oldAuthPattern.test(patched)) {
|
|
87
|
+
patched = patched.replace(oldAuthPattern, `${guardBlock} const repo = getRepo();`);
|
|
88
|
+
} else {
|
|
89
|
+
patched = patched.replace(
|
|
90
|
+
" const repo = getRepo();",
|
|
91
|
+
`${guardBlock} const repo = getRepo();`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (patched === existing) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
await writeFile(path, patched, "utf-8");
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function patchRunsRouteMutationGuardIfMissing(path) {
|
|
106
|
+
try {
|
|
107
|
+
const existing = await readFile(path, "utf-8");
|
|
108
|
+
if (
|
|
109
|
+
existing.includes("Same-origin guard for mutating runs endpoint") &&
|
|
110
|
+
existing.includes("export async function DELETE(request") &&
|
|
111
|
+
existing.indexOf("Same-origin guard for mutating runs endpoint") >
|
|
112
|
+
existing.indexOf("export async function DELETE(request")
|
|
113
|
+
) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const guardBlock = [
|
|
118
|
+
" // Same-origin guard for mutating runs endpoint (no app auth integration required)",
|
|
119
|
+
" const requestOrigin = new URL(request.url).origin;",
|
|
120
|
+
' const fetchSite = request.headers.get("sec-fetch-site");',
|
|
121
|
+
' const origin = request.headers.get("origin");',
|
|
122
|
+
' const referer = request.headers.get("referer");',
|
|
123
|
+
" const hasMatchingOrigin = origin === requestOrigin;",
|
|
124
|
+
" const hasMatchingReferer = (() => {",
|
|
125
|
+
" if (!referer) return false;",
|
|
126
|
+
" try {",
|
|
127
|
+
" return new URL(referer).origin === requestOrigin;",
|
|
128
|
+
" } catch {",
|
|
129
|
+
" return false;",
|
|
130
|
+
" }",
|
|
131
|
+
" })();",
|
|
132
|
+
' const hasSameOriginBrowserSignal = fetchSite === "same-origin";',
|
|
133
|
+
" const hasValidSameOriginSignal = hasSameOriginBrowserSignal || hasMatchingOrigin || hasMatchingReferer;",
|
|
134
|
+
" const secret = process.env.ANTEATER_SECRET;",
|
|
135
|
+
' const hasValidSecret = !!secret && request.headers.get("x-anteater-secret") === secret;',
|
|
136
|
+
" if (!hasValidSameOriginSignal && !hasValidSecret) {",
|
|
137
|
+
' return NextResponse.json({ error: "Forbidden" }, { status: 403 });',
|
|
138
|
+
" }",
|
|
139
|
+
"",
|
|
140
|
+
].join("\n");
|
|
141
|
+
|
|
142
|
+
const deleteFnPattern =
|
|
143
|
+
/(export async function DELETE\(request[^\n]*\) \{[\s\S]*?if \(!requestId\) \{[\s\S]*?\n \}\n\s*)/;
|
|
144
|
+
let patched = existing;
|
|
145
|
+
|
|
146
|
+
if (deleteFnPattern.test(existing)) {
|
|
147
|
+
patched = existing.replace(deleteFnPattern, `$1${guardBlock}`);
|
|
148
|
+
} else {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Clean up buggy older patch where guard was accidentally inserted in GET.
|
|
153
|
+
const getGuardPattern =
|
|
154
|
+
/(export async function GET\(\) \{[\s\S]*?)\n \/\/ Same-origin guard for mutating runs endpoint[\s\S]*? const gh = \(url/g;
|
|
155
|
+
if (getGuardPattern.test(patched)) {
|
|
156
|
+
patched = patched.replace(getGuardPattern, "$1\n const gh = (url");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (patched === existing) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
await writeFile(path, patched, "utf-8");
|
|
163
|
+
return true;
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
32
169
|
/**
|
|
33
170
|
* Generate anteater.config.ts
|
|
34
171
|
*/
|
|
@@ -121,6 +258,14 @@ export function generateApiRoute({ isTypeScript, productionBranch }) {
|
|
|
121
258
|
// --- POST handler ---
|
|
122
259
|
add("export async function POST(request" + (TS ? ": NextRequest" : "") + ") {");
|
|
123
260
|
add(" try {");
|
|
261
|
+
add(' const contentType = request.headers.get("content-type") || "";');
|
|
262
|
+
add(' if (!contentType.toLowerCase().includes("application/json")) {');
|
|
263
|
+
add(" return NextResponse.json" + (TS ? "<AnteaterResponse>" : "") + "(");
|
|
264
|
+
add(' { requestId: "", branch: "", status: "error", error: "Content-Type must be application/json" },');
|
|
265
|
+
add(" { status: 415 }");
|
|
266
|
+
add(" );");
|
|
267
|
+
add(" }");
|
|
268
|
+
add("");
|
|
124
269
|
add(" const body" + (TS ? ": AnteaterRequest" : "") + " = await request.json();");
|
|
125
270
|
add("");
|
|
126
271
|
add(" if (!body.prompt?.trim()) {");
|
|
@@ -130,20 +275,29 @@ export function generateApiRoute({ isTypeScript, productionBranch }) {
|
|
|
130
275
|
add(" );");
|
|
131
276
|
add(" }");
|
|
132
277
|
add("");
|
|
133
|
-
add(" //
|
|
134
|
-
add(" const
|
|
135
|
-
add(
|
|
136
|
-
add('
|
|
137
|
-
add('
|
|
138
|
-
add("
|
|
139
|
-
add(
|
|
140
|
-
add("
|
|
141
|
-
add("
|
|
142
|
-
add(
|
|
143
|
-
add("
|
|
144
|
-
add("
|
|
145
|
-
add(" }");
|
|
278
|
+
add(" // Same-origin guard for mutating Anteater requests (no app auth integration required)");
|
|
279
|
+
add(" const requestOrigin = request.nextUrl.origin;");
|
|
280
|
+
add(' const fetchSite = request.headers.get("sec-fetch-site");');
|
|
281
|
+
add(' const origin = request.headers.get("origin");');
|
|
282
|
+
add(' const referer = request.headers.get("referer");');
|
|
283
|
+
add(" const hasMatchingOrigin = origin === requestOrigin;");
|
|
284
|
+
add(" const hasMatchingReferer = (() => {");
|
|
285
|
+
add(" if (!referer) return false;");
|
|
286
|
+
add(" try {");
|
|
287
|
+
add(" return new URL(referer).origin === requestOrigin;");
|
|
288
|
+
add(" } catch {");
|
|
289
|
+
add(" return false;");
|
|
146
290
|
add(" }");
|
|
291
|
+
add(" })();");
|
|
292
|
+
add(' const hasSameOriginBrowserSignal = fetchSite === "same-origin";');
|
|
293
|
+
add(" const hasValidSameOriginSignal = hasSameOriginBrowserSignal || hasMatchingOrigin || hasMatchingReferer;");
|
|
294
|
+
add(" const secret = process.env.ANTEATER_SECRET;");
|
|
295
|
+
add(' const hasValidSecret = !!secret && request.headers.get("x-anteater-secret") === secret;');
|
|
296
|
+
add(" if (!hasValidSameOriginSignal && !hasValidSecret) {");
|
|
297
|
+
add(" return NextResponse.json" + (TS ? "<AnteaterResponse>" : "") + "(");
|
|
298
|
+
add(' { requestId: "", branch: "", status: "error", error: "Forbidden" },');
|
|
299
|
+
add(" { status: 403 }");
|
|
300
|
+
add(" );");
|
|
147
301
|
add(" }");
|
|
148
302
|
add("");
|
|
149
303
|
add(" const repo = getRepo();");
|
|
@@ -656,6 +810,28 @@ function buildRunsDeleteHandlerLines(TS) {
|
|
|
656
810
|
add(" return NextResponse.json({ error: \"requestId is required\" }, { status: 400 });");
|
|
657
811
|
add(" }");
|
|
658
812
|
add("");
|
|
813
|
+
add(" // Same-origin guard for mutating runs endpoint (no app auth integration required)");
|
|
814
|
+
add(" const requestOrigin = new URL(request.url).origin;");
|
|
815
|
+
add(' const fetchSite = request.headers.get("sec-fetch-site");');
|
|
816
|
+
add(' const origin = request.headers.get("origin");');
|
|
817
|
+
add(' const referer = request.headers.get("referer");');
|
|
818
|
+
add(" const hasMatchingOrigin = origin === requestOrigin;");
|
|
819
|
+
add(" const hasMatchingReferer = (() => {");
|
|
820
|
+
add(" if (!referer) return false;");
|
|
821
|
+
add(" try {");
|
|
822
|
+
add(" return new URL(referer).origin === requestOrigin;");
|
|
823
|
+
add(" } catch {");
|
|
824
|
+
add(" return false;");
|
|
825
|
+
add(" }");
|
|
826
|
+
add(" })();");
|
|
827
|
+
add(' const hasSameOriginBrowserSignal = fetchSite === "same-origin";');
|
|
828
|
+
add(" const hasValidSameOriginSignal = hasSameOriginBrowserSignal || hasMatchingOrigin || hasMatchingReferer;");
|
|
829
|
+
add(" const secret = process.env.ANTEATER_SECRET;");
|
|
830
|
+
add(' const hasValidSecret = !!secret && request.headers.get("x-anteater-secret") === secret;');
|
|
831
|
+
add(" if (!hasValidSameOriginSignal && !hasValidSecret) {");
|
|
832
|
+
add(' return NextResponse.json({ error: "Forbidden" }, { status: 403 });');
|
|
833
|
+
add(" }");
|
|
834
|
+
add("");
|
|
659
835
|
add(" const gh = (url" + (TS ? ": string" : "") + ", options" + (TS ? "?: RequestInit" : "") + ") =>");
|
|
660
836
|
add(" fetch(url, {");
|
|
661
837
|
add(" ...options,");
|
|
@@ -887,18 +1063,27 @@ export async function scaffoldFiles(cwd, options) {
|
|
|
887
1063
|
const route = generateApiRoute(options);
|
|
888
1064
|
const routeDir = options.isAppRouter ? "app/api/anteater" : "pages/api/anteater";
|
|
889
1065
|
const routePath = join(cwd, routeDir, route.filename);
|
|
890
|
-
|
|
1066
|
+
const createdRoute = await writeIfNotExists(routePath, route.content);
|
|
1067
|
+
if (createdRoute) {
|
|
891
1068
|
results.push(join(routeDir, route.filename));
|
|
1069
|
+
} else if (await patchApiRouteMutationGuardIfMissing(routePath)) {
|
|
1070
|
+
results.push(`${join(routeDir, route.filename)} (patched same-origin guard)`);
|
|
892
1071
|
}
|
|
893
1072
|
|
|
894
1073
|
// Runs API route (multi-run discovery)
|
|
895
1074
|
const runsRoute = generateRunsRoute(options);
|
|
896
1075
|
const runsDir = options.isAppRouter ? "app/api/anteater/runs" : "pages/api/anteater/runs";
|
|
897
1076
|
const runsPath = join(cwd, runsDir, runsRoute.filename);
|
|
898
|
-
|
|
1077
|
+
const createdRunsRoute = await writeIfNotExists(runsPath, runsRoute.content);
|
|
1078
|
+
if (createdRunsRoute) {
|
|
899
1079
|
results.push(join(runsDir, runsRoute.filename));
|
|
900
|
-
} else
|
|
901
|
-
|
|
1080
|
+
} else {
|
|
1081
|
+
if (await patchRunsRouteDeleteIfMissing(runsPath, options.isTypeScript)) {
|
|
1082
|
+
results.push(`${join(runsDir, runsRoute.filename)} (patched DELETE handler)`);
|
|
1083
|
+
}
|
|
1084
|
+
if (await patchRunsRouteMutationGuardIfMissing(runsPath)) {
|
|
1085
|
+
results.push(`${join(runsDir, runsRoute.filename)} (patched same-origin guard)`);
|
|
1086
|
+
}
|
|
902
1087
|
}
|
|
903
1088
|
|
|
904
1089
|
// GitHub Action workflow
|