@1agh/maude 0.58.2 → 0.58.3

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 (105) hide show
  1. package/apps/studio/annotations-bindings.ts +83 -4
  2. package/apps/studio/api.ts +6 -1
  3. package/apps/studio/bin/_fetch-asset.mjs +169 -5
  4. package/apps/studio/bin/_import-asset.mjs +72 -0
  5. package/apps/studio/bin/_import-figma.mjs +1121 -0
  6. package/apps/studio/bin/_video-playwright.mjs +86 -3
  7. package/apps/studio/bin/import-figma.sh +38 -0
  8. package/apps/studio/bin/read-annotations.mjs +11 -1
  9. package/apps/studio/bun.lock +16 -22
  10. package/apps/studio/canvas-edit.ts +29 -5
  11. package/apps/studio/client/app.jsx +44 -1
  12. package/apps/studio/client/export-center.jsx +42 -4
  13. package/apps/studio/client/panels/CloudBar.jsx +92 -1
  14. package/apps/studio/client/panels/FigmaImportPanel.jsx +264 -0
  15. package/apps/studio/client/panels/GitPanel.jsx +26 -6
  16. package/apps/studio/client/panels/SettingsPanel.jsx +181 -0
  17. package/apps/studio/client/panels/SetupChecklist.jsx +26 -2
  18. package/apps/studio/client/panels/TimelinePanel.jsx +2 -2
  19. package/apps/studio/client/panels/timeline-parse.js +3 -3
  20. package/apps/studio/client/styles/3-shell-maude.css +7 -0
  21. package/apps/studio/client/styles/4-components.css +134 -0
  22. package/apps/studio/clip-ops.ts +93 -17
  23. package/apps/studio/cloud/endpoints.ts +78 -10
  24. package/apps/studio/cloud/renew.ts +183 -0
  25. package/apps/studio/context.ts +2 -1
  26. package/apps/studio/dist/client.bundle.js +1261 -1261
  27. package/apps/studio/dist/runtime/@remotion_media.js +56 -136
  28. package/apps/studio/dist/runtime/@remotion_player.js +18 -18
  29. package/apps/studio/dist/runtime/@remotion_transitions.js +9 -9
  30. package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -1
  31. package/apps/studio/dist/runtime/remotion.js +12 -12
  32. package/apps/studio/dist/styles.css +1 -1
  33. package/apps/studio/exporters/_browser-bundles.ts +20 -6
  34. package/apps/studio/exporters/_runtime.ts +19 -0
  35. package/apps/studio/exporters/degraded.ts +92 -0
  36. package/apps/studio/exporters/index.ts +5 -0
  37. package/apps/studio/exporters/jobs.ts +19 -0
  38. package/apps/studio/exporters/unsupported-media.ts +170 -0
  39. package/apps/studio/exporters/video-encode-lib.ts +27 -1
  40. package/apps/studio/exporters/video-render-lib.ts +6 -0
  41. package/apps/studio/exporters/video.ts +62 -1
  42. package/apps/studio/figma/assets.test.ts +372 -0
  43. package/apps/studio/figma/assets.ts +398 -0
  44. package/apps/studio/figma/client.test.ts +395 -0
  45. package/apps/studio/figma/client.ts +513 -0
  46. package/apps/studio/figma/comments-to-strokes.test.ts +194 -0
  47. package/apps/studio/figma/comments-to-strokes.ts +173 -0
  48. package/apps/studio/figma/endpoints.ts +200 -0
  49. package/apps/studio/figma/sanitize.test.ts +256 -0
  50. package/apps/studio/figma/sanitize.ts +315 -0
  51. package/apps/studio/figma/style-map.ts +352 -0
  52. package/apps/studio/figma/to-artboard.test.ts +808 -0
  53. package/apps/studio/figma/to-artboard.ts +701 -0
  54. package/apps/studio/figma/to-render.test.ts +180 -0
  55. package/apps/studio/figma/to-render.ts +306 -0
  56. package/apps/studio/figma/to-strokes-roundtrip.test.ts +152 -0
  57. package/apps/studio/figma/to-strokes.test.ts +705 -0
  58. package/apps/studio/figma/to-strokes.ts +749 -0
  59. package/apps/studio/figma/to-tokens.test.ts +321 -0
  60. package/apps/studio/figma/to-tokens.ts +305 -0
  61. package/apps/studio/figma/types.ts +539 -0
  62. package/apps/studio/figma/url.test.ts +167 -0
  63. package/apps/studio/figma/url.ts +160 -0
  64. package/apps/studio/http.ts +129 -0
  65. package/apps/studio/sync/asset-push.ts +124 -0
  66. package/apps/studio/sync/connection-state.ts +11 -0
  67. package/apps/studio/sync/hub-link.ts +63 -7
  68. package/apps/studio/sync/hubs-config.ts +31 -3
  69. package/apps/studio/sync/index.ts +276 -26
  70. package/apps/studio/sync/migrate-flat-fallback.ts +121 -0
  71. package/apps/studio/sync/presentation.ts +45 -1
  72. package/apps/studio/sync/supervisor.ts +5 -1
  73. package/apps/studio/sync/workspace-signin.ts +7 -3
  74. package/apps/studio/test/annotations-bindings.test.ts +150 -12
  75. package/apps/studio/test/canvas-create-api.test.ts +4 -1
  76. package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
  77. package/apps/studio/test/capture-determinism-shape.test.ts +135 -0
  78. package/apps/studio/test/clip-addressing.test.ts +6 -1
  79. package/apps/studio/test/clip-ops.test.ts +5 -1
  80. package/apps/studio/test/cloud-endpoints.test.ts +96 -0
  81. package/apps/studio/test/cloud-renew.test.ts +205 -0
  82. package/apps/studio/test/cloud-shell-surfaces.test.ts +11 -2
  83. package/apps/studio/test/exporters/degraded-propagation.test.ts +123 -0
  84. package/apps/studio/test/exporters/unsupported-media.test.ts +123 -0
  85. package/apps/studio/test/fetch-asset-gate.test.ts +189 -0
  86. package/apps/studio/test/figma-provenance.test.ts +108 -0
  87. package/apps/studio/test/figma-routes.test.ts +294 -0
  88. package/apps/studio/test/git-cloud-posture.test.ts +50 -0
  89. package/apps/studio/test/hub-link.test.ts +11 -0
  90. package/apps/studio/test/import-figma.test.ts +479 -0
  91. package/apps/studio/test/sync-asset-push.test.ts +124 -0
  92. package/apps/studio/test/sync-connection-state.test.ts +13 -0
  93. package/apps/studio/test/sync-hubs-config.test.ts +5 -0
  94. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +98 -0
  95. package/apps/studio/test/sync-path-pull.test.ts +63 -0
  96. package/apps/studio/test/sync-presentation.test.ts +77 -0
  97. package/apps/studio/test/sync-runtime.test.ts +316 -1
  98. package/apps/studio/test/video-comp.test.ts +23 -1
  99. package/apps/studio/test/workspace-containment.test.ts +1 -0
  100. package/apps/studio/video-comp.tsx +70 -6
  101. package/apps/studio/whats-new.json +27 -0
  102. package/apps/studio/workspace-mode.ts +4 -0
  103. package/cli/commands/design.mjs +8 -0
  104. package/package.json +8 -8
  105. package/plugins/flow/.claude-plugin/config.schema.json +3 -3
@@ -0,0 +1,183 @@
1
+ // Silent hub-credential renewal — the missing half of the cell-token model.
2
+ //
3
+ // The cell session token dies with the 12 h project token that minted it
4
+ // (Phase 23 B2 — deliberate: that cap IS the revocation window, and it stays).
5
+ // The account/device token next to it never expires. Nothing connected the
6
+ // two: the desktop held a forever credential and a 12 h credential side by
7
+ // side, and when the short one died the user was told to press Connect again —
8
+ // or, worse, told nothing (the alligators incident: 73 canvases permanently
9
+ // `connecting…`).
10
+ //
11
+ // This module replays the attach lane's credential tail WITHOUT the ceremony:
12
+ // GET /api/projects (account token) → which project owns this hub
13
+ // POST /projects/open (account token) → fresh 12 h project token
14
+ // POST <cell>/auth/login (project token) → fresh cell session
15
+ // saveHubCredential(…) → hubs.json, role preserved
16
+ //
17
+ // It deliberately does NOT touch `.design/config.json` (the link is already
18
+ // recorded; renewal is not a re-link) and does NOT cycle the sync runtime (the
19
+ // caller swaps the token in place). A renewal fails only on a REAL event —
20
+ // signed out, token revoked on the Account page, removed from the project —
21
+ // and that failure, not a clock, is what should reach the user.
22
+ //
23
+ // Trust posture: every hop talks only to the CONFIGURED cloud address or to
24
+ // the workspace URL the cloud's own /api/projects listing names for the
25
+ // matched project — never to an address read from repo-committed state.
26
+
27
+ import { existsSync, readFileSync } from 'node:fs';
28
+ import { homedir } from 'node:os';
29
+ import { join } from 'node:path';
30
+
31
+ import { saveHubCredential } from '../sync/hub-link.ts';
32
+ import { getHubRecord, normalizeUrl } from '../sync/hubs-config.ts';
33
+
34
+ export interface RenewedCredential {
35
+ token: string;
36
+ expiresAt: number | null;
37
+ }
38
+
39
+ export type RenewFailureReason =
40
+ | 'not-signed-in'
41
+ | 'account-revoked'
42
+ | 'no-matching-project'
43
+ | 'open-refused'
44
+ | 'cell-refused'
45
+ | 'save-failed'
46
+ | 'unreachable';
47
+
48
+ export type RenewResult =
49
+ | ({ ok: true } & RenewedCredential)
50
+ | { ok: false; reason: RenewFailureReason };
51
+
52
+ /** Mirrors cloud/endpoints.ts — resolved per call so tests/desktop can set it late. */
53
+ function cloudUrl(): string {
54
+ return (process.env.MAUDE_CLOUD_URL ?? 'https://cloud.maude.sh').replace(/\/+$/, '');
55
+ }
56
+
57
+ function cloudConfigPath(): string {
58
+ return process.env.MAUDE_CLOUD_CONFIG ?? join(homedir(), '.config', 'maude', 'cloud.json');
59
+ }
60
+
61
+ /** The account credential, or null when this machine is signed out. */
62
+ function readAccountToken(): string | null {
63
+ const p = cloudConfigPath();
64
+ if (!existsSync(p)) return null;
65
+ try {
66
+ const parsed = JSON.parse(readFileSync(p, 'utf8'));
67
+ return parsed && typeof parsed.token === 'string' && parsed.token ? parsed.token : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ const FETCH_TIMEOUT_MS = 15_000;
74
+
75
+ async function jsonFetch(
76
+ doFetch: typeof fetch,
77
+ url: string,
78
+ init: RequestInit = {}
79
+ ): Promise<{ status: number; body: Record<string, unknown> }> {
80
+ const ctl = new AbortController();
81
+ const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
82
+ try {
83
+ const res = await doFetch(url, { ...init, signal: ctl.signal });
84
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
85
+ return { status: res.status, body };
86
+ } catch {
87
+ return { status: 0, body: {} };
88
+ } finally {
89
+ clearTimeout(timer);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Mint a fresh cell session for `hubUrl` from the signed-in Maude account.
95
+ *
96
+ * Single network path, no interaction. Callers treat any `ok:false` as "keep
97
+ * the old behaviour" — the stored (possibly dead) credential stays in place so
98
+ * a failed renewal never makes things worse than before it ran.
99
+ */
100
+ export async function renewHubCredential(
101
+ hubUrl: string,
102
+ doFetch: typeof fetch = fetch
103
+ ): Promise<RenewResult> {
104
+ const account = readAccountToken();
105
+ if (!account) return { ok: false, reason: 'not-signed-in' };
106
+
107
+ let norm: string;
108
+ try {
109
+ norm = normalizeUrl(hubUrl);
110
+ } catch {
111
+ return { ok: false, reason: 'no-matching-project' };
112
+ }
113
+
114
+ // Which cloud project opens onto this hub? The listing is the only mapping
115
+ // that exists (the hub URL is derived from the project id at open time), and
116
+ // asking it fresh means a renamed/removed project fails here — the right
117
+ // place — instead of minting for a project this account no longer has.
118
+ const listed = await jsonFetch(doFetch, `${cloudUrl()}/api/projects`, {
119
+ headers: { authorization: `Bearer ${account}` },
120
+ });
121
+ if (listed.status === 401) return { ok: false, reason: 'account-revoked' };
122
+ if (listed.status !== 200) return { ok: false, reason: 'unreachable' };
123
+ const projects = Array.isArray(listed.body.projects) ? listed.body.projects : [];
124
+ const match = projects.find((p) => {
125
+ const url = (p as { url?: unknown })?.url;
126
+ if (typeof url !== 'string' || !url) return false;
127
+ try {
128
+ return normalizeUrl(url) === norm;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }) as { id?: unknown; role?: unknown } | undefined;
133
+ // A viewer's Connect is a different lane (share-view); renewing an editor
134
+ // credential for one would mint capability the panel never offered.
135
+ if (!match || typeof match.id !== 'string' || match.role === 'viewer') {
136
+ return { ok: false, reason: 'no-matching-project' };
137
+ }
138
+
139
+ const opened = await jsonFetch(doFetch, `${cloudUrl()}/projects/open`, {
140
+ method: 'POST',
141
+ headers: { authorization: `Bearer ${account}`, 'content-type': 'application/json' },
142
+ body: JSON.stringify({ project: match.id }),
143
+ });
144
+ if (opened.status === 0) return { ok: false, reason: 'unreachable' };
145
+ if (opened.status !== 200 || typeof opened.body.token !== 'string' || !opened.body.token) {
146
+ // This is where a removed member's renewal dies — exactly where it should.
147
+ return { ok: false, reason: 'open-refused' };
148
+ }
149
+
150
+ const cellLogin = await jsonFetch(doFetch, `${norm}/auth/login`, {
151
+ method: 'POST',
152
+ headers: { 'content-type': 'application/json' },
153
+ body: JSON.stringify({ token: opened.body.token }),
154
+ });
155
+ if (cellLogin.status === 0) return { ok: false, reason: 'unreachable' };
156
+ if (
157
+ cellLogin.status !== 200 ||
158
+ typeof cellLogin.body.token !== 'string' ||
159
+ !cellLogin.body.token
160
+ ) {
161
+ return { ok: false, reason: 'cell-refused' };
162
+ }
163
+ const expiresAt =
164
+ typeof cellLogin.body.expiresAt === 'number' && Number.isFinite(cellLogin.body.expiresAt)
165
+ ? cellLogin.body.expiresAt
166
+ : null;
167
+
168
+ // The upsert replaces the whole record, so the role must be re-supplied. F5:
169
+ // prefer the role the cell JUST vouched (its /auth/login echoes `user.role`)
170
+ // over the stored one — this is the one moment the code learns the current
171
+ // role, and carrying the stale value forward would discard exactly that.
172
+ // Fall back to the prior role only when the cell didn't say (older cell).
173
+ const freshRoleRaw = (cellLogin.body.user as { role?: unknown } | undefined)?.role;
174
+ const freshRole = typeof freshRoleRaw === 'string' && freshRoleRaw ? freshRoleRaw : undefined;
175
+ const role = freshRole ?? getHubRecord(norm)?.role;
176
+ try {
177
+ saveHubCredential(norm, cellLogin.body.token, role, expiresAt ?? undefined);
178
+ } catch {
179
+ return { ok: false, reason: 'save-failed' };
180
+ }
181
+
182
+ return { ok: true, token: cellLogin.body.token, expiresAt };
183
+ }
@@ -191,7 +191,8 @@ export interface Context {
191
191
  * non-serving entry points construct a Context without one.
192
192
  */
193
193
  syncControl?: {
194
- restart(linkedHub?: LinkedHub): Promise<{
194
+ /** `null` means UNLINK: clear the in-memory link and cycle back to solo. */
195
+ restart(linkedHub?: LinkedHub | null): Promise<{
195
196
  syncing: boolean;
196
197
  canvases: number;
197
198
  reason?: string;