@1agh/maude 0.54.0 → 0.55.0
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/annotations-layer.tsx +11 -1
- package/apps/studio/canvas-build-sandbox.ts +101 -9
- package/apps/studio/canvas-build-worker.ts +7 -1
- package/apps/studio/canvas-build.ts +9 -1
- package/apps/studio/client/app.jsx +18 -0
- package/apps/studio/client/github.js +7 -0
- package/apps/studio/client/panels/CloudBar.jsx +461 -40
- package/apps/studio/client/styles/3-shell-maude.css +30 -0
- package/apps/studio/cloud/endpoints.ts +48 -6
- package/apps/studio/dist/client.bundle.js +581 -581
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/http.ts +14 -1
- package/apps/studio/test/cloud-endpoints.test.ts +231 -4
- package/apps/studio/whats-new.json +18 -0
- package/package.json +8 -8
package/apps/studio/http.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { type Api, ASSET_MAX_BYTES, ASSET_MAX_VIDEO_BYTES } from './api.ts';
|
|
|
22
22
|
import { ImportAssetError, importSvg, SVG_MAX_BYTES } from './bin/_import-asset.mjs';
|
|
23
23
|
import { ImportBrandError, importBrand } from './bin/_import-brand.mjs';
|
|
24
24
|
import { buildCanvasModule } from './canvas-build.ts';
|
|
25
|
-
import { buildCanvasSandboxed } from './canvas-build-sandbox.ts';
|
|
25
|
+
import { buildCanvasSandboxed, buildStats } from './canvas-build-sandbox.ts';
|
|
26
26
|
import { canvasLibPath } from './canvas-lib-resolver.ts';
|
|
27
27
|
import { TranspileError } from './canvas-pipeline.ts';
|
|
28
28
|
import { createCloudEndpoints } from './cloud/endpoints.ts';
|
|
@@ -1108,6 +1108,19 @@ export function createHttp(
|
|
|
1108
1108
|
// supervisor hashes its own expected root and compares.
|
|
1109
1109
|
rootId: rootIdentity(ctx.paths.repoRoot),
|
|
1110
1110
|
pid: process.pid,
|
|
1111
|
+
// Cloud Phase 26 Stage 4 — the build sandbox's own counters, so the
|
|
1112
|
+
// €3/cell model finally has figures instead of a prediction. Counts and
|
|
1113
|
+
// durations ONLY: never a canvas name, never a path, and never the text
|
|
1114
|
+
// of a rejected import specifier (that specifier is tenant-authored
|
|
1115
|
+
// content — the operational fact is that a rejection happened, not what
|
|
1116
|
+
// it said).
|
|
1117
|
+
//
|
|
1118
|
+
// On `/_health`, which the untrusted canvas origin may read, and that
|
|
1119
|
+
// is deliberate: every figure here is about the reader's OWN cell, so
|
|
1120
|
+
// it discloses nothing across a tenant boundary. Inventing a privileged
|
|
1121
|
+
// route would have meant inventing an auth path for the supervisor's
|
|
1122
|
+
// loopback probe, for data the tenant already has.
|
|
1123
|
+
render: buildStats(),
|
|
1111
1124
|
}),
|
|
1112
1125
|
|
|
1113
1126
|
'/_active': () => Response.json(inspect().state),
|
|
@@ -9,7 +9,12 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isDisplayableUrl,
|
|
14
|
+
localIdentityHint,
|
|
15
|
+
parseDeepLink,
|
|
16
|
+
shareViewUrl,
|
|
17
|
+
} from '../client/panels/CloudBar.jsx';
|
|
13
18
|
|
|
14
19
|
let stub: ReturnType<typeof Bun.serve> | null = null;
|
|
15
20
|
let scratch = '';
|
|
@@ -78,20 +83,242 @@ describe('parseDeepLink — untrusted input, strict shape', () => {
|
|
|
78
83
|
'maude://open/x?code=nothex', // bad code
|
|
79
84
|
'https://evil.example/?code=mhc_' + 'a'.repeat(64), // not maude://
|
|
80
85
|
'maude://join/x/y', // join is not the open shape
|
|
86
|
+
// Over the platform's 40-char project-id cap — a drive-by link never
|
|
87
|
+
// passes through the server-side bound, so arbitrary attacker text would
|
|
88
|
+
// otherwise render in the dialog heading, prose and aria-label.
|
|
89
|
+
`maude://open/${'a'.repeat(41)}?code=mhc_${'a'.repeat(64)}`,
|
|
81
90
|
]) {
|
|
82
91
|
expect(parseDeepLink(bad)).toBeNull();
|
|
83
92
|
}
|
|
93
|
+
// …and exactly 40 is still a real project id.
|
|
94
|
+
expect(
|
|
95
|
+
parseDeepLink(`maude://open/${'a'.repeat(40)}?code=mhc_${'a'.repeat(64)}`)
|
|
96
|
+
).not.toBeNull();
|
|
84
97
|
});
|
|
85
98
|
});
|
|
86
99
|
|
|
87
100
|
describe('shareViewUrl — the viewer’s browser home', () => {
|
|
88
|
-
test('
|
|
101
|
+
test('is the project’s OWN address — the view- gallery is gone (Phase 25 C5)', () => {
|
|
102
|
+
// `view-*` is hard-refused by tenantFromHostname, so the old derivation
|
|
103
|
+
// produced a guaranteed 404 — invisible only because window.open was a no-op
|
|
104
|
+
// in the shell. Making the opener work would have made it a dead menu item.
|
|
89
105
|
expect(shareViewUrl('https://alligators.cloud.maude.sh', 'alligators')).toBe(
|
|
90
|
-
'https://
|
|
106
|
+
'https://alligators.cloud.maude.sh'
|
|
91
107
|
);
|
|
108
|
+
expect(shareViewUrl('https://alligators.cloud.maude.sh', 'alligators')).not.toContain('view-');
|
|
92
109
|
});
|
|
93
110
|
test('falls back to the platform zone on garbage', () => {
|
|
94
|
-
expect(shareViewUrl('nonsense', 'p1')).toBe('https://
|
|
111
|
+
expect(shareViewUrl('nonsense', 'p1')).toBe('https://p1.cloud.maude.sh');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('isDisplayableUrl — what may be rendered as a clickable address', () => {
|
|
116
|
+
const CLOUD = 'https://cloud.maude.sh';
|
|
117
|
+
|
|
118
|
+
test('the configured origin and the cloud zone’s front doors pass', () => {
|
|
119
|
+
expect(isDisplayableUrl('https://cloud.maude.sh/activate?code=ABCD', CLOUD)).toBe(true);
|
|
120
|
+
expect(isDisplayableUrl('https://alligators.cloud.maude.sh/', CLOUD)).toBe(true);
|
|
121
|
+
// A self-host origin is legitimate for its OWN addresses.
|
|
122
|
+
expect(isDisplayableUrl('http://127.0.0.1:8788/activate', 'http://127.0.0.1:8788')).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('an off-zone address is never rendered — a scheme check was not enough', () => {
|
|
126
|
+
// The bug this closes: `https://evil.example/activate?code=…` from a hostile
|
|
127
|
+
// or MITM'd control plane passed a protocol-only check and still rendered as
|
|
128
|
+
// a live href with a Copy button, and middle-click never reaches the click
|
|
129
|
+
// handler, let alone the Rust zone lock.
|
|
130
|
+
for (const bad of [
|
|
131
|
+
'https://evil.example/activate?code=x',
|
|
132
|
+
'https://cloud.maude.sh.attacker.example/',
|
|
133
|
+
'javascript:alert(1)',
|
|
134
|
+
'file:///etc/passwd',
|
|
135
|
+
'not a url',
|
|
136
|
+
]) {
|
|
137
|
+
expect(isDisplayableUrl(bad, CLOUD)).toBe(false);
|
|
138
|
+
}
|
|
139
|
+
// Deep paths on a TENANT host are refused here too — same rule as the opener.
|
|
140
|
+
expect(isDisplayableUrl('https://canvas-evil.cloud.maude.sh/stored.svg', CLOUD)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('status — names the LOCAL half so a deep link can be read', () => {
|
|
145
|
+
test('reports the open folder and its workspace, and never a credential', async () => {
|
|
146
|
+
const { createCloudEndpoints } = await import('../cloud/endpoints.ts');
|
|
147
|
+
const { mkdirSync } = await import('node:fs');
|
|
148
|
+
const repoRoot = join(scratch, 'alligators-web');
|
|
149
|
+
const designRoot = join(repoRoot, '.design');
|
|
150
|
+
mkdirSync(designRoot, { recursive: true });
|
|
151
|
+
writeFileSync(
|
|
152
|
+
join(designRoot, 'config.json'),
|
|
153
|
+
JSON.stringify({ linkedHub: { url: 'https://alligators.cloud.maude.sh', linkedAt: 1 } })
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const r = createCloudEndpoints({ paths: { repoRoot, designRoot } }).status();
|
|
157
|
+
expect(r.status).toBe(200);
|
|
158
|
+
const json = r.json as Record<string, unknown>;
|
|
159
|
+
expect(json.project).toBe('alligators-web');
|
|
160
|
+
// No hub credential was stored in this scratch dir, so the link is reported
|
|
161
|
+
// but explicitly UNCORROBORATED — the dialog may not vouch for it.
|
|
162
|
+
expect(json.linkedHub).toEqual({
|
|
163
|
+
url: 'https://alligators.cloud.maude.sh',
|
|
164
|
+
credentialed: false,
|
|
165
|
+
});
|
|
166
|
+
// Signed out here — and the payload must stay free of token material
|
|
167
|
+
// whatever the state (this response reaches the browser surface too).
|
|
168
|
+
expect(json.connected).toBe(false);
|
|
169
|
+
expect(JSON.stringify(json)).not.toContain('token');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('an unlinked (or malformed-config) folder reports linkedHub: null, not an error', async () => {
|
|
173
|
+
const { createCloudEndpoints } = await import('../cloud/endpoints.ts');
|
|
174
|
+
const { mkdirSync } = await import('node:fs');
|
|
175
|
+
const repoRoot = join(scratch, 'fresh-thing');
|
|
176
|
+
const designRoot = join(repoRoot, '.design');
|
|
177
|
+
mkdirSync(designRoot, { recursive: true });
|
|
178
|
+
writeFileSync(join(designRoot, 'config.json'), '{ not json');
|
|
179
|
+
|
|
180
|
+
const json = createCloudEndpoints({ paths: { repoRoot, designRoot } }).status().json as Record<
|
|
181
|
+
string,
|
|
182
|
+
unknown
|
|
183
|
+
>;
|
|
184
|
+
expect(json.linkedHub).toBeNull();
|
|
185
|
+
expect(json.project).toBe('fresh-thing');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('localIdentityHint — only an exact, corroborated agreement is silent', () => {
|
|
190
|
+
const CLOUD = 'https://cloud.maude.sh';
|
|
191
|
+
|
|
192
|
+
test('the reported case warns: one project open, the link naming another', () => {
|
|
193
|
+
// StudyFi open, "Open in Maude" pressed on alligators — one click from
|
|
194
|
+
// syncing StudyFi's designs into somebody else's workspace.
|
|
195
|
+
expect(
|
|
196
|
+
localIdentityHint({ localProject: 'AI-StudyMate', cloudUrl: CLOUD, claimed: 'alligators' })
|
|
197
|
+
).toBe('mismatch');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('a name an ATTACKER chose to look like yours does not buy silence', () => {
|
|
201
|
+
// The whole point (attacker pass B1). The cloud project id is registerable by
|
|
202
|
+
// anyone, so treating "contains your folder name" as agreement handed the
|
|
203
|
+
// attacker the off-switch for the warning. And the server's 409 cannot help:
|
|
204
|
+
// the claim and the actual project agree — it IS their project.
|
|
205
|
+
for (const claimed of [
|
|
206
|
+
'alligators-design-sync',
|
|
207
|
+
'alligators-backup',
|
|
208
|
+
'shared-alligators',
|
|
209
|
+
'web-design-studio-app',
|
|
210
|
+
]) {
|
|
211
|
+
expect(localIdentityHint({ localProject: 'alligators', cloudUrl: CLOUD, claimed })).not.toBe(
|
|
212
|
+
'match'
|
|
213
|
+
);
|
|
214
|
+
expect(
|
|
215
|
+
['similar', 'mismatch'].includes(
|
|
216
|
+
localIdentityHint({ localProject: 'alligators', cloudUrl: CLOUD, claimed })
|
|
217
|
+
)
|
|
218
|
+
).toBe(true);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('an exact name is the only silent name-based verdict', () => {
|
|
223
|
+
expect(
|
|
224
|
+
localIdentityHint({ localProject: 'alligators', cloudUrl: CLOUD, claimed: 'alligators' })
|
|
225
|
+
).toBe('match');
|
|
226
|
+
// Normalization still applies — casing and separators are not a difference.
|
|
227
|
+
expect(
|
|
228
|
+
localIdentityHint({ localProject: 'Alligators', cloudUrl: CLOUD, claimed: 'alligators' })
|
|
229
|
+
).toBe('match');
|
|
230
|
+
// A near-match is its own state: alike, and still NOT the same workspace.
|
|
231
|
+
expect(
|
|
232
|
+
localIdentityHint({ localProject: 'alligators-web', cloudUrl: CLOUD, claimed: 'alligators' })
|
|
233
|
+
).toBe('similar');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('a CREDENTIALED link to that project reads as linked', () => {
|
|
237
|
+
expect(
|
|
238
|
+
localIdentityHint({
|
|
239
|
+
localProject: 'anything',
|
|
240
|
+
linkedHubUrl: 'https://alligators.cloud.maude.sh',
|
|
241
|
+
linkedHubCredentialed: true,
|
|
242
|
+
cloudUrl: CLOUD,
|
|
243
|
+
claimed: 'alligators',
|
|
244
|
+
})
|
|
245
|
+
).toBe('linked');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('an UNCREDENTIALED linkedHub cannot vouch for anyone', () => {
|
|
249
|
+
// config.json is committed and travels with the repo, so a hostile template
|
|
250
|
+
// could otherwise make the dialog say "already linked to <attacker>" — the
|
|
251
|
+
// strongest reassurance it can give (attacker pass B2). Without a stored
|
|
252
|
+
// credential it is ignored and the folder name decides.
|
|
253
|
+
expect(
|
|
254
|
+
localIdentityHint({
|
|
255
|
+
localProject: 'my-thing',
|
|
256
|
+
linkedHubUrl: 'https://attacker.cloud.maude.sh',
|
|
257
|
+
linkedHubCredentialed: false,
|
|
258
|
+
cloudUrl: CLOUD,
|
|
259
|
+
claimed: 'attacker',
|
|
260
|
+
})
|
|
261
|
+
).toBe('mismatch');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('a folder credentialed to a DIFFERENT workspace warns', () => {
|
|
265
|
+
expect(
|
|
266
|
+
localIdentityHint({
|
|
267
|
+
localProject: 'alligators',
|
|
268
|
+
linkedHubUrl: 'https://someone-else.cloud.maude.sh',
|
|
269
|
+
linkedHubCredentialed: true,
|
|
270
|
+
cloudUrl: CLOUD,
|
|
271
|
+
claimed: 'alligators',
|
|
272
|
+
})
|
|
273
|
+
).toBe('mismatch');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('a short generic folder name does not buy agreement by containment', () => {
|
|
277
|
+
for (const localProject of ['web', 'app', 'src']) {
|
|
278
|
+
expect(
|
|
279
|
+
localIdentityHint({ localProject, cloudUrl: CLOUD, claimed: 'webshop-frontend' })
|
|
280
|
+
).toBe('mismatch');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('an accented or over-long folder name still matches its own workspace', () => {
|
|
285
|
+
// The fold must mirror the platform's deriveProjectId (NFKD + strip + 40-cap)
|
|
286
|
+
// or the warning fires on every LEGITIMATE connect for a whole class of
|
|
287
|
+
// names — which is how a consent warning dies (attacker re-review).
|
|
288
|
+
expect(
|
|
289
|
+
localIdentityHint({ localProject: 'Zkušební tým', cloudUrl: CLOUD, claimed: 'zkusebni-tym' })
|
|
290
|
+
).toBe('match');
|
|
291
|
+
expect(
|
|
292
|
+
localIdentityHint({ localProject: 'Přátelé', cloudUrl: CLOUD, claimed: 'pratele' })
|
|
293
|
+
).toBe('match');
|
|
294
|
+
// >40 chars: the platform truncates the id, so the fold must truncate too.
|
|
295
|
+
const long = 'a-very-long-project-name-that-the-platform-truncates-at-forty';
|
|
296
|
+
expect(
|
|
297
|
+
localIdentityHint({ localProject: long, cloudUrl: CLOUD, claimed: long.slice(0, 40) })
|
|
298
|
+
).toBe('match');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('no signal is "unknown", which warns — it must never read as agreement', () => {
|
|
302
|
+
// Every stub and self-hoster is http://127.0.0.1:<port>, carrying no project
|
|
303
|
+
// name. Before, that produced a reassuring neutral with a primary Connect;
|
|
304
|
+
// on the launch path an unresolved status did the same (attacker pass B3).
|
|
305
|
+
expect(
|
|
306
|
+
localIdentityHint({
|
|
307
|
+
localProject: '',
|
|
308
|
+
linkedHubUrl: 'http://127.0.0.1:4599',
|
|
309
|
+
cloudUrl: 'http://127.0.0.1:4599',
|
|
310
|
+
claimed: 'stub-project',
|
|
311
|
+
})
|
|
312
|
+
).toBe('unknown');
|
|
313
|
+
// Status not yet resolved → unknown, whatever else is on hand.
|
|
314
|
+
expect(
|
|
315
|
+
localIdentityHint({
|
|
316
|
+
localProject: 'alligators',
|
|
317
|
+
cloudUrl: CLOUD,
|
|
318
|
+
claimed: 'alligators',
|
|
319
|
+
resolved: false,
|
|
320
|
+
})
|
|
321
|
+
).toBe('unknown');
|
|
95
322
|
});
|
|
96
323
|
});
|
|
97
324
|
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "cloud-connect-browser-open-and-decision-modal",
|
|
6
|
+
"version": "0.55.0",
|
|
7
|
+
"date": "2026-08-05",
|
|
8
|
+
"kind": "fix",
|
|
9
|
+
"title": "Signing in opens your browser again",
|
|
10
|
+
"summary": "Signing in to Maude Cloud from the app used to go nowhere — the browser never opened, the dialog claimed it had, and you were left holding a code. It opens now, straight to the page with your code already filled in, and the address is always there to click or copy if it doesn't. \"Open in Maude\" also asks a clearer question: it names the folder you have open and the cloud project it would connect it to, says exactly what syncs, and warns you if the two don't look like the same thing.",
|
|
11
|
+
"surface": "design-ui"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "activity-says-why-we-looked",
|
|
15
|
+
"version": "0.55.0",
|
|
16
|
+
"date": "2026-08-05",
|
|
17
|
+
"kind": "improvement",
|
|
18
|
+
"title": "Your project's activity says why we looked",
|
|
19
|
+
"summary": "The activity page for a cloud project used to show that the platform touched it. Now it shows the reason given at the time, in plain words — and it includes the platform-wide checks that sweep every project, which were previously the only ones you could not see.",
|
|
20
|
+
"surface": "design-ui"
|
|
21
|
+
},
|
|
4
22
|
{
|
|
5
23
|
"id": "your-own-place-in-a-shared-project",
|
|
6
24
|
"version": "0.54.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
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": {
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"test:e2e:desktop:parity:cloud": "pnpm --filter @maude/desktop-e2e e2e:parity:cloud"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
58
|
-
"@1agh/maude-darwin-x64": "0.
|
|
59
|
-
"@1agh/maude-linux-arm64": "0.
|
|
60
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
61
|
-
"@1agh/maude-linux-x64": "0.
|
|
62
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
63
|
-
"@1agh/maude-win32-x64": "0.
|
|
57
|
+
"@1agh/maude-darwin-arm64": "0.55.0",
|
|
58
|
+
"@1agh/maude-darwin-x64": "0.55.0",
|
|
59
|
+
"@1agh/maude-linux-arm64": "0.55.0",
|
|
60
|
+
"@1agh/maude-linux-arm64-musl": "0.55.0",
|
|
61
|
+
"@1agh/maude-linux-x64": "0.55.0",
|
|
62
|
+
"@1agh/maude-linux-x64-musl": "0.55.0",
|
|
63
|
+
"@1agh/maude-win32-x64": "0.55.0"
|
|
64
64
|
},
|
|
65
65
|
"files": [
|
|
66
66
|
"cli",
|