@1agh/maude 0.53.0 → 0.53.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/apps/studio/bin/_import-asset.mjs +1 -6
- package/apps/studio/bin/_import-brand.mjs +1 -1
- package/apps/studio/bin/_video-playwright.mjs +5 -1
- package/apps/studio/dist/client.bundle.js +1 -1
- package/apps/studio/http.ts +13 -3
- package/apps/studio/test/report-proxy.test.ts +125 -0
- package/package.json +8 -8
package/apps/studio/http.ts
CHANGED
|
@@ -1263,7 +1263,13 @@ export function createHttp(
|
|
|
1263
1263
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1264
1264
|
const endpoint = process.env.MAUDE_REPORT_URL || 'https://cloud.maude.sh/report';
|
|
1265
1265
|
try {
|
|
1266
|
-
|
|
1266
|
+
// `globalThis.fetch` — NOT bare `fetch`: this module declares its own
|
|
1267
|
+
// `async function fetch(req: Request)` (the server fall-through handler)
|
|
1268
|
+
// in the same scope, which shadows the global. A bare call re-entered the
|
|
1269
|
+
// fall-through with a URL string, so `new URL(req.url)` threw
|
|
1270
|
+
// `"undefined" cannot be parsed as a URL` and every report died with a
|
|
1271
|
+
// plain-text HTTP 500 without ever leaving the machine.
|
|
1272
|
+
const upstream = await globalThis.fetch(endpoint, {
|
|
1267
1273
|
method: 'POST',
|
|
1268
1274
|
body: await req.formData(),
|
|
1269
1275
|
signal: AbortSignal.timeout(30_000),
|
|
@@ -3948,7 +3954,11 @@ export function createHttp(
|
|
|
3948
3954
|
'/index.html': () => serveFile(join(CLIENT_DIR, 'index.html')),
|
|
3949
3955
|
} satisfies Record<string, (req: Request) => Response | Promise<Response>>;
|
|
3950
3956
|
|
|
3951
|
-
|
|
3957
|
+
// Named `handleFallthrough`, not `fetch` — a same-named local function shadows
|
|
3958
|
+
// the global `fetch` for every call site in this module's scope, which is how
|
|
3959
|
+
// the `/_api/report` route ended up calling itself instead of the network (see
|
|
3960
|
+
// the `globalThis.fetch` comment on that route; RCA issue-report-a-bug-http-500).
|
|
3961
|
+
async function handleFallthrough(req: Request): Promise<Response> {
|
|
3952
3962
|
try {
|
|
3953
3963
|
const url = new URL(req.url);
|
|
3954
3964
|
const pathname = url.pathname;
|
|
@@ -4342,7 +4352,7 @@ export function createHttp(
|
|
|
4342
4352
|
) as typeof routes;
|
|
4343
4353
|
|
|
4344
4354
|
async function guardedFetch(req: Request): Promise<Response> {
|
|
4345
|
-
return readOnlyRefusal(req) ??
|
|
4355
|
+
return readOnlyRefusal(req) ?? handleFallthrough(req);
|
|
4346
4356
|
}
|
|
4347
4357
|
|
|
4348
4358
|
return { routes: guardedRoutes, fetch: guardedFetch, serveCanvasShell, isCanvasSafeRoute };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Regression test for issue-report-a-bug-http-500 (RCA:
|
|
2
|
+
// .ai/logs/rca/issue-report-a-bug-http-500.md). `/_api/report` proxies the
|
|
3
|
+
// dialog's multipart body to `MAUDE_REPORT_URL`. A bare `fetch(endpoint, …)`
|
|
4
|
+
// call inside http.ts silently resolved to this module's OWN same-named
|
|
5
|
+
// fall-through handler instead of the global `fetch` — the request never left
|
|
6
|
+
// the machine, and the route mirrored a bogus local 500 back to the client.
|
|
7
|
+
// These tests boot a real server pointed at a real stub upstream, so a
|
|
8
|
+
// reintroduced shadow (or any other break in the proxy hop) fails loudly
|
|
9
|
+
// instead of only being caught by a mocked-upstream unit test.
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from 'bun:test';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
|
|
15
|
+
|
|
16
|
+
async function bootStubUpstream(
|
|
17
|
+
handler: (req: Request) => Response | Promise<Response>
|
|
18
|
+
): Promise<{ url: string; stop: () => void; received: () => number }> {
|
|
19
|
+
let received = 0;
|
|
20
|
+
const server = Bun.serve({
|
|
21
|
+
port: 0,
|
|
22
|
+
fetch: async (req) => {
|
|
23
|
+
received += 1;
|
|
24
|
+
return handler(req);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
29
|
+
stop: () => server.stop(true),
|
|
30
|
+
received: () => received,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('/_api/report proxy hop', () => {
|
|
35
|
+
test('forwards the multipart body verbatim and mirrors a successful upstream response', async () => {
|
|
36
|
+
const { root } = makeSandbox();
|
|
37
|
+
let capturedBytes = 0;
|
|
38
|
+
let capturedHasReportPart = false;
|
|
39
|
+
const upstream = await bootStubUpstream(async (req) => {
|
|
40
|
+
const form = await req.formData();
|
|
41
|
+
capturedHasReportPart = typeof form.get('report') === 'string';
|
|
42
|
+
// Multipart parsing consumed the body — approximate "did real bytes
|
|
43
|
+
// arrive" by re-serializing what FormData kept.
|
|
44
|
+
capturedBytes = [...form.values()].reduce(
|
|
45
|
+
(n, v) => n + (typeof v === 'string' ? v.length : (v as Blob).size),
|
|
46
|
+
0
|
|
47
|
+
);
|
|
48
|
+
return Response.json({ ok: true, issueNumber: 1, issueUrl: 'https://example.com/issues/1' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const port = nextPort();
|
|
52
|
+
const proc = await bootServer(root, port, { MAUDE_REPORT_URL: `${upstream.url}/report` });
|
|
53
|
+
try {
|
|
54
|
+
const form = new FormData();
|
|
55
|
+
form.set(
|
|
56
|
+
'report',
|
|
57
|
+
JSON.stringify({ schema: 'maude-report/v1', description: 'test', reportId: 'r-0123abcd' })
|
|
58
|
+
);
|
|
59
|
+
form.append('screenshot', new Blob([new Uint8Array([1, 2, 3, 4])]), 'shot.png');
|
|
60
|
+
|
|
61
|
+
const res = await fetch(`http://localhost:${port}/_api/report`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body: form,
|
|
64
|
+
});
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
const json = await res.json();
|
|
67
|
+
expect(json).toEqual({ ok: true, issueNumber: 1, issueUrl: 'https://example.com/issues/1' });
|
|
68
|
+
|
|
69
|
+
expect(upstream.received()).toBe(1);
|
|
70
|
+
expect(capturedHasReportPart).toBe(true);
|
|
71
|
+
expect(capturedBytes).toBeGreaterThan(0);
|
|
72
|
+
} finally {
|
|
73
|
+
await killProc(proc);
|
|
74
|
+
upstream.stop();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('an unreachable upstream yields 502 with a JSON error, not a bare 500', async () => {
|
|
79
|
+
const { root } = makeSandbox();
|
|
80
|
+
const port = nextPort();
|
|
81
|
+
// Nothing listens on this port — the fetch itself must fail/timeout.
|
|
82
|
+
const proc = await bootServer(root, port, { MAUDE_REPORT_URL: 'http://127.0.0.1:1/report' });
|
|
83
|
+
try {
|
|
84
|
+
const form = new FormData();
|
|
85
|
+
form.set(
|
|
86
|
+
'report',
|
|
87
|
+
JSON.stringify({ schema: 'maude-report/v1', description: 'test', reportId: 'r-0123abcd' })
|
|
88
|
+
);
|
|
89
|
+
const res = await fetch(`http://localhost:${port}/_api/report`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: form,
|
|
92
|
+
});
|
|
93
|
+
expect(res.status).toBe(502);
|
|
94
|
+
const json = await res.json();
|
|
95
|
+
expect(json).toEqual({ error: 'report service unreachable' });
|
|
96
|
+
} finally {
|
|
97
|
+
await killProc(proc);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('no bare `fetch(` call sites in http.ts', () => {
|
|
103
|
+
test('every fetch call is either the global fetch (qualified) or a local handler name', () => {
|
|
104
|
+
const src = readFileSync(new URL('../http.ts', import.meta.url), 'utf8');
|
|
105
|
+
// A bare `fetch(` immediately preceded by a word-boundary and NOT preceded
|
|
106
|
+
// by a `.` (member access) or `function ` (declaration) would silently
|
|
107
|
+
// resolve to this file's own same-named fall-through handler instead of
|
|
108
|
+
// the global — exactly the bug this test guards against. `handleFallthrough`
|
|
109
|
+
// is the current, unambiguous name; this assertion also fails if the
|
|
110
|
+
// handler is ever renamed back to something call-site-ambiguous.
|
|
111
|
+
const bareFetchCalls = [...src.matchAll(/(?<![.\w])fetch\(/g)];
|
|
112
|
+
const lines = src.split('\n');
|
|
113
|
+
// Skip: comment lines (mentioning `fetch(` in prose is fine) and the one
|
|
114
|
+
// legitimate bare occurrence — the type-signature parameter name
|
|
115
|
+
// `fetch(req: Request): Promise<Response>;` in the `Api`-shaped interface,
|
|
116
|
+
// which is a declaration, not a call.
|
|
117
|
+
const offendingLines = bareFetchCalls
|
|
118
|
+
.map((m) => src.slice(0, m.index).split('\n').length)
|
|
119
|
+
.map((line) => ({ line, text: lines[line - 1] ?? '' }))
|
|
120
|
+
.filter(({ text }) => !/^\s*(\/\/|\*)/.test(text))
|
|
121
|
+
.filter(({ text }) => !/^\s*fetch\(req: Request\): Promise<Response>;/.test(text));
|
|
122
|
+
|
|
123
|
+
expect(offendingLines).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.53.
|
|
3
|
+
"version": "0.53.1",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"test:e2e:desktop:acp-cold-start": "pnpm --filter @maude/desktop-e2e e2e:acp-cold-start"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
|
-
"@1agh/maude-darwin-arm64": "0.53.
|
|
56
|
-
"@1agh/maude-darwin-x64": "0.53.
|
|
57
|
-
"@1agh/maude-linux-arm64": "0.53.
|
|
58
|
-
"@1agh/maude-linux-arm64-musl": "0.53.
|
|
59
|
-
"@1agh/maude-linux-x64": "0.53.
|
|
60
|
-
"@1agh/maude-linux-x64-musl": "0.53.
|
|
61
|
-
"@1agh/maude-win32-x64": "0.53.
|
|
55
|
+
"@1agh/maude-darwin-arm64": "0.53.1",
|
|
56
|
+
"@1agh/maude-darwin-x64": "0.53.1",
|
|
57
|
+
"@1agh/maude-linux-arm64": "0.53.1",
|
|
58
|
+
"@1agh/maude-linux-arm64-musl": "0.53.1",
|
|
59
|
+
"@1agh/maude-linux-x64": "0.53.1",
|
|
60
|
+
"@1agh/maude-linux-x64-musl": "0.53.1",
|
|
61
|
+
"@1agh/maude-win32-x64": "0.53.1"
|
|
62
62
|
},
|
|
63
63
|
"files": [
|
|
64
64
|
"cli",
|