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.
Files changed (2) hide show
  1. package/lib/scaffold.mjs +202 -17
  2. 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(" // Auth: sec-fetch-site for same-origin (AnteaterBar), x-anteater-secret for external");
134
- add(" const secret = process.env.ANTEATER_SECRET;");
135
- add(" if (secret) {");
136
- add(' const fetchSite = request.headers.get("sec-fetch-site");');
137
- add(' const isSameOrigin = fetchSite === "same-origin";');
138
- add(" if (!isSameOrigin) {");
139
- add(' const authHeader = request.headers.get("x-anteater-secret");');
140
- add(" if (authHeader !== secret) {");
141
- add(" return NextResponse.json" + (TS ? "<AnteaterResponse>" : "") + "(");
142
- add(' { requestId: "", branch: "", status: "error", error: "Unauthorized" },');
143
- add(" { status: 401 }");
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
- if (await writeIfNotExists(routePath, route.content)) {
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
- if (await writeIfNotExists(runsPath, runsRoute.content)) {
1077
+ const createdRunsRoute = await writeIfNotExists(runsPath, runsRoute.content);
1078
+ if (createdRunsRoute) {
899
1079
  results.push(join(runsDir, runsRoute.filename));
900
- } else if (await patchRunsRouteDeleteIfMissing(runsPath, options.isTypeScript)) {
901
- results.push(`${join(runsDir, runsRoute.filename)} (patched DELETE handler)`);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-anteater",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "AI-powered live editing for your Next.js app",
5
5
  "bin": {
6
6
  "anteater": "bin/setup-anteater.mjs",