@1agh/maude 0.47.0 → 0.49.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/README.md +7 -6
- package/apps/studio/acp/bridge.ts +8 -1
- package/apps/studio/acp/plugin-bootstrap.ts +15 -1
- package/apps/studio/api.ts +18 -2
- package/apps/studio/assets-s3.ts +291 -0
- package/apps/studio/build.ts +1 -1
- package/apps/studio/client/export-center.jsx +7 -6
- package/apps/studio/client/github.js +11 -4
- package/apps/studio/collab/origins.ts +150 -0
- package/apps/studio/collab/protocol.ts +36 -9
- package/apps/studio/collab/room.ts +124 -0
- package/apps/studio/context.ts +9 -0
- package/apps/studio/dist/client.bundle.js +282 -282
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/REMOTION-LICENSE.md +1 -1
- package/apps/studio/examples/perf-100-artboards.tsx +1 -1
- package/apps/studio/exporters/pdf.ts +35 -1
- package/apps/studio/git/service.ts +4 -1
- package/apps/studio/http.ts +45 -0
- package/apps/studio/paths.ts +37 -1
- package/apps/studio/server.ts +75 -2
- package/apps/studio/sync/autocommit.ts +299 -0
- package/apps/studio/sync/doc-name.ts +228 -0
- package/apps/studio/sync/index.ts +95 -1
- package/apps/studio/sync/workspace-signin.ts +301 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +34 -0
- package/apps/studio/test/acp-session-plugins.test.ts +6 -0
- package/apps/studio/test/assets-s3.test.ts +249 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +7 -0
- package/apps/studio/test/collab-origin-gate.test.ts +323 -0
- package/apps/studio/test/exporters/pdf.test.ts +33 -1
- package/apps/studio/test/sync-autocommit.test.ts +334 -0
- package/apps/studio/test/sync-doc-name.test.ts +281 -0
- package/apps/studio/test/workspace-containment.test.ts +258 -0
- package/apps/studio/test/workspace-signin.test.ts +270 -0
- package/apps/studio/use-collab.tsx +28 -1
- package/apps/studio/workspace-mode.ts +210 -0
- package/apps/studio/ws.ts +11 -2
- package/cli/bin/maude.mjs +1 -0
- package/cli/commands/hub-workspace.mjs +341 -0
- package/cli/commands/hub.mjs +325 -3
- package/cli/commands/hub.test.mjs +14 -1
- package/cli/commands/init.mjs +80 -3
- package/cli/commands/kg.mjs +368 -0
- package/cli/commands/kg.test.mjs +118 -0
- package/cli/lib/cell-plan.mjs +302 -0
- package/cli/lib/cell-plan.test.mjs +225 -0
- package/cli/lib/ddr-to-kgai.mjs +648 -0
- package/cli/lib/ddr-to-kgai.test.mjs +99 -0
- package/cli/lib/flow-design-integration.test.mjs +2 -2
- package/cli/lib/gitignore-block.mjs +16 -1
- package/cli/lib/plugin-name-namespace.test.mjs +71 -0
- package/cli/lib/workspace-plan.mjs +422 -0
- package/cli/lib/workspace-plan.test.mjs +223 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +17 -0
- package/plugins/flow/.claude-plugin/config.schema.json +66 -0
- package/plugins/flow/dependencies.json +17 -0
|
@@ -71,6 +71,40 @@ export function encodeAwarenessFrame(
|
|
|
71
71
|
return encoding.toUint8Array(encoder);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Peek the message type of a frame without consuming it. Lets a caller route
|
|
76
|
+
* sync vs awareness before deciding *which doc* to apply the frame to — the
|
|
77
|
+
* origin gate (`collab/origins.ts`) validates sync frames against a mirror doc
|
|
78
|
+
* first, and needs to know a frame is a sync frame to do so.
|
|
79
|
+
*/
|
|
80
|
+
export function readMessageType(payload: Uint8Array): number {
|
|
81
|
+
return decoding.readVarUint(decoding.createDecoder(payload));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply one MESSAGE_SYNC frame to `doc` with `origin` as the Y transaction
|
|
86
|
+
* origin, returning the reply frame (sync step 2 / ack) if there is one.
|
|
87
|
+
*
|
|
88
|
+
* Split out of `handleMessage` so the same bytes can be applied to a throwaway
|
|
89
|
+
* mirror doc under a probe origin (the origin gate) before they are allowed
|
|
90
|
+
* anywhere near the real room doc. Non-sync payloads return `null` untouched.
|
|
91
|
+
*/
|
|
92
|
+
export function applySyncMessage(
|
|
93
|
+
payload: Uint8Array,
|
|
94
|
+
doc: Y.Doc,
|
|
95
|
+
origin: unknown
|
|
96
|
+
): Uint8Array | null {
|
|
97
|
+
const decoder = decoding.createDecoder(payload);
|
|
98
|
+
if (decoding.readVarUint(decoder) !== MESSAGE_SYNC) return null;
|
|
99
|
+
const encoder = encoding.createEncoder();
|
|
100
|
+
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
|
101
|
+
// readSyncMessage applies the peer's update to the doc and writes the
|
|
102
|
+
// response (sync step 2 / sync step 2 ack) into encoder.
|
|
103
|
+
syncProtocol.readSyncMessage(decoder, encoder, doc, origin);
|
|
104
|
+
if (encoding.length(encoder) > 1) return encoding.toUint8Array(encoder);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
/**
|
|
75
109
|
* Decode and dispatch one incoming binary frame. Returns an optional reply
|
|
76
110
|
* frame the caller MUST send back to the originating peer (sync step 2 from
|
|
@@ -89,15 +123,8 @@ export function handleMessage(
|
|
|
89
123
|
const messageType = decoding.readVarUint(decoder);
|
|
90
124
|
|
|
91
125
|
switch (messageType) {
|
|
92
|
-
case MESSAGE_SYNC:
|
|
93
|
-
|
|
94
|
-
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
|
95
|
-
// readSyncMessage applies the peer's update to the doc and writes the
|
|
96
|
-
// response (sync step 2 / sync step 2 ack) into encoder.
|
|
97
|
-
syncProtocol.readSyncMessage(decoder, encoder, doc, conn);
|
|
98
|
-
if (encoding.length(encoder) > 1) return encoding.toUint8Array(encoder);
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
126
|
+
case MESSAGE_SYNC:
|
|
127
|
+
return applySyncMessage(payload, doc, conn);
|
|
101
128
|
case MESSAGE_AWARENESS: {
|
|
102
129
|
awarenessProtocol.applyAwarenessUpdate(awareness, decoding.readVarUint8Array(decoder), conn);
|
|
103
130
|
return null;
|
|
@@ -13,17 +13,34 @@ import path from 'node:path';
|
|
|
13
13
|
import { Awareness, removeAwarenessStates } from 'y-protocols/awareness';
|
|
14
14
|
import * as Y from 'yjs';
|
|
15
15
|
|
|
16
|
+
import { isBodyLane, rootTypesTouched } from './origins.ts';
|
|
16
17
|
import {
|
|
18
|
+
applySyncMessage,
|
|
17
19
|
type CollabConn,
|
|
18
20
|
encodeAwarenessFrame,
|
|
19
21
|
encodeHandshake,
|
|
20
22
|
encodeSyncUpdate,
|
|
21
23
|
handleMessage,
|
|
24
|
+
MESSAGE_SYNC,
|
|
25
|
+
readMessageType,
|
|
22
26
|
} from './protocol.ts';
|
|
23
27
|
|
|
24
28
|
export interface RoomConn extends CollabConn {
|
|
25
29
|
/** Stable id for this connection (UUID from ws.data.id). */
|
|
26
30
|
id: string;
|
|
31
|
+
/**
|
|
32
|
+
* Which server origin upgraded this socket (DDR-063 canvas-origin split).
|
|
33
|
+
* `'canvas'` = the untrusted canvas iframe origin — its sync frames go
|
|
34
|
+
* through the origin gate (DDR-122 follow-up) and may never write a body
|
|
35
|
+
* lane. Absent or `'main'` = the privileged shell origin, ungated.
|
|
36
|
+
*
|
|
37
|
+
* Absent defaults to trusted ON PURPOSE: every pre-existing caller (tests,
|
|
38
|
+
* the shell socket) is main-origin, and the two canvas-origin call sites
|
|
39
|
+
* (`server.ts`'s `startCanvasServer` upgrade → `ws.ts`'s `bindCollab`) set
|
|
40
|
+
* it explicitly. `test/collab-origin-gate.test.ts` pins that wiring so the
|
|
41
|
+
* default can't silently swallow a future canvas path.
|
|
42
|
+
*/
|
|
43
|
+
realm?: 'main' | 'canvas';
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
export interface RoomCallbacks {
|
|
@@ -72,6 +89,12 @@ export interface Room {
|
|
|
72
89
|
destroy(): Promise<void>;
|
|
73
90
|
/** Test/inspection: connection count. */
|
|
74
91
|
size(): number;
|
|
92
|
+
/**
|
|
93
|
+
* Test/inspection: how many canvas-realm sync frames the origin gate has
|
|
94
|
+
* refused for touching a body lane. Non-zero in production means untrusted
|
|
95
|
+
* canvas script tried to write source — worth surfacing, never expected.
|
|
96
|
+
*/
|
|
97
|
+
gateRefusals(): number;
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
const DEBOUNCE_MS = 800;
|
|
@@ -133,9 +156,104 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
133
156
|
}
|
|
134
157
|
}
|
|
135
158
|
|
|
159
|
+
// --- Origin gate (DDR-122 follow-up) -------------------------------------
|
|
160
|
+
//
|
|
161
|
+
// A canvas-realm sync frame is applied to `mirror` FIRST, under a probe
|
|
162
|
+
// origin, so `transaction.changed` tells us which root lanes it targets.
|
|
163
|
+
// Only if it touches no body lane do the same bytes reach `doc`. See
|
|
164
|
+
// `collab/origins.ts` for why the update bytes alone can't answer this.
|
|
165
|
+
const GATE_PROBE_ORIGIN = Object.freeze({ maudeOrigin: 'collab-gate-probe' });
|
|
166
|
+
const GATE_MIRROR_SYNC = Object.freeze({ maudeOrigin: 'collab-gate-mirror-sync' });
|
|
167
|
+
let mirror: Y.Doc | null = null;
|
|
168
|
+
let probeRoots: Set<string> | null = null;
|
|
169
|
+
let gateRefusalCount = 0;
|
|
170
|
+
|
|
171
|
+
function ensureMirror(): Y.Doc {
|
|
172
|
+
if (mirror) return mirror;
|
|
173
|
+
const m = new Y.Doc();
|
|
174
|
+
Y.applyUpdate(m, Y.encodeStateAsUpdate(doc), GATE_MIRROR_SYNC);
|
|
175
|
+
m.on('afterTransaction', (tr: Y.Transaction) => {
|
|
176
|
+
if (tr.origin === GATE_PROBE_ORIGIN) probeRoots = rootTypesTouched(tr);
|
|
177
|
+
});
|
|
178
|
+
mirror = m;
|
|
179
|
+
return m;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Drop a mirror that has been poisoned by a refused frame. Rebuilt lazily
|
|
183
|
+
* from the (still clean) room doc on the next canvas-realm frame. */
|
|
184
|
+
function discardMirror(): void {
|
|
185
|
+
const m = mirror;
|
|
186
|
+
mirror = null;
|
|
187
|
+
if (m) {
|
|
188
|
+
try {
|
|
189
|
+
m.destroy();
|
|
190
|
+
} catch {
|
|
191
|
+
/* best effort */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Canvas-realm frame path. Awareness frames are unchanged (cursors/presence
|
|
198
|
+
* are ephemeral and already server-attributed). Sync frames are gated.
|
|
199
|
+
*/
|
|
200
|
+
function receiveGated(conn: RoomConn, payload: Uint8Array): void {
|
|
201
|
+
if (readMessageType(payload) !== MESSAGE_SYNC) {
|
|
202
|
+
const reply = handleMessage(payload, doc, awareness, conn);
|
|
203
|
+
if (reply) conn.send(reply);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const m = ensureMirror();
|
|
208
|
+
probeRoots = null;
|
|
209
|
+
let probeThrew = false;
|
|
210
|
+
try {
|
|
211
|
+
applySyncMessage(payload, m, GATE_PROBE_ORIGIN);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
probeThrew = true;
|
|
214
|
+
console.error(`[collab/${slug}] origin gate: probe apply failed:`, err);
|
|
215
|
+
}
|
|
216
|
+
// Fail closed on anything we could not positively classify — a probe that
|
|
217
|
+
// threw, or a root type we could not resolve to a name.
|
|
218
|
+
const roots = probeRoots ?? new Set<string>();
|
|
219
|
+
const refused = [...roots].filter((r) => isBodyLane(r) || r === '<unresolved>');
|
|
220
|
+
if (probeThrew || refused.length > 0) {
|
|
221
|
+
gateRefusalCount += 1;
|
|
222
|
+
discardMirror();
|
|
223
|
+
console.warn(
|
|
224
|
+
`[collab/${slug}] origin gate REFUSED a canvas-realm sync frame ` +
|
|
225
|
+
`(lanes: ${refused.join(', ') || 'unclassifiable'}). Untrusted canvas script may ` +
|
|
226
|
+
`not write canvas source — see DDR-122 follow-up / collab/origins.ts.`
|
|
227
|
+
);
|
|
228
|
+
// Re-assert server truth to that peer so it converges on the authoritative
|
|
229
|
+
// state instead of silently diverging. Their locally-authored body items
|
|
230
|
+
// stay in their own realm (they made them) but never reach disk or a peer.
|
|
231
|
+
try {
|
|
232
|
+
conn.send(encodeSyncUpdate(Y.encodeStateAsUpdate(doc)));
|
|
233
|
+
} catch {
|
|
234
|
+
/* close handler will clean up */
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const reply = applySyncMessage(payload, doc, conn);
|
|
240
|
+
if (reply) conn.send(reply);
|
|
241
|
+
}
|
|
242
|
+
|
|
136
243
|
// Y.Doc update -> broadcast to all peers + schedule debounced flush.
|
|
137
244
|
doc.on('update', (update: Uint8Array, origin: unknown) => {
|
|
138
245
|
if (destroyed) return;
|
|
246
|
+
// Keep the gate mirror in lockstep with the room doc, so the next probe is
|
|
247
|
+
// evaluated against current truth. Idempotent — a re-applied update is a
|
|
248
|
+
// no-op in yjs, which is what makes the "probe then apply for real" double
|
|
249
|
+
// application safe.
|
|
250
|
+
if (mirror) {
|
|
251
|
+
try {
|
|
252
|
+
Y.applyUpdate(mirror, update, GATE_MIRROR_SYNC);
|
|
253
|
+
} catch {
|
|
254
|
+
discardMirror();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
139
257
|
// Don't echo back to the origin — they already have it; reduces noise.
|
|
140
258
|
const except =
|
|
141
259
|
origin && typeof origin === 'object' && 'id' in origin ? (origin as RoomConn) : undefined;
|
|
@@ -197,6 +315,10 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
197
315
|
}
|
|
198
316
|
|
|
199
317
|
function receive(conn: RoomConn, payload: Uint8Array): void {
|
|
318
|
+
if (conn.realm === 'canvas') {
|
|
319
|
+
receiveGated(conn, payload);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
200
322
|
const reply = handleMessage(payload, doc, awareness, conn);
|
|
201
323
|
if (reply) conn.send(reply);
|
|
202
324
|
}
|
|
@@ -235,6 +357,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
235
357
|
}
|
|
236
358
|
if (dirty) await flush();
|
|
237
359
|
conns.clear();
|
|
360
|
+
discardMirror();
|
|
238
361
|
awareness.destroy();
|
|
239
362
|
doc.destroy();
|
|
240
363
|
}
|
|
@@ -250,6 +373,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
250
373
|
flush,
|
|
251
374
|
destroy,
|
|
252
375
|
size: () => conns.size,
|
|
376
|
+
gateRefusals: () => gateRefusalCount,
|
|
253
377
|
};
|
|
254
378
|
}
|
|
255
379
|
|
package/apps/studio/context.ts
CHANGED
|
@@ -38,6 +38,15 @@ export interface LinkedHub {
|
|
|
38
38
|
* preserved. Only for hubs you operate or fully trust.
|
|
39
39
|
*/
|
|
40
40
|
syncTsx?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* DDR-192 §5 — stable workspace identity for the hub document namespace
|
|
43
|
+
* (`ws/<workspace-id>/<branch>/<slug>`). Authoritative when present; the
|
|
44
|
+
* cloud control plane sets it at provisioning time. Absent, the sync runtime
|
|
45
|
+
* derives it from the git `origin` remote — and if there is no origin either,
|
|
46
|
+
* stays on legacy flat slugs rather than inventing a per-machine id that
|
|
47
|
+
* would split peers of the same project apart. See sync/doc-name.ts.
|
|
48
|
+
*/
|
|
49
|
+
workspaceId?: string;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
export interface DevServerConfig {
|