Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.17.0] - 2026-08-14
Added
- Pure TypeScript Companion Core Architecture (
src/companion-core/) — extracted all companion runtime logic into modular, testable pure TypeScript modules (types.ts,validation.ts,protocol.ts,token.ts,session.ts,relay.ts,theme.ts,runtime.ts). - Automated Companion Bundler Pipeline (
scripts/build-companion.mjs) — introduced standaloneesbuildcompilation pipeline compiling TypeScript entrypoints into zero-dependency, single-file HTML distributions (public/figma-companion-ui.htmlandpublic/penpot-companion-ui.html), wired intoyarn buildandyarn dev. - Comprehensive Companion Unit Test Suite (
src/companion-core/__tests__/) — created 6 new unit test suites with 100% test coverage across validation, protocol parsing, token exchange, session lifecycle, and state machine transitions (170/170 passing tests). - Target Design Parity for Companions — aligned Figma and Penpot companion interfaces with the Miro/FigJam design system:
- 2-Tab Layout (
SYNCandSETTINGS) matchingTabNav.tsxwith uppercase monospace tracking and cyan bottom indicator (no icons). - ThemeToggle Cycle Button matching
ThemeToggle.tsx(System→Light→Dark→Systemwith Monitor, Sun, and Moon vector icons). - Button Token Harmonization (
CONNECT/COPY IDmatchingtext-[9px] font-mono font-bold tracking-wider text-accent border border-accent/40 rounded px-1.5 py-0.5;DISCONNECTas borderless underline link). - Clean Status Indicators without box/pill badge outlines (
text-[9px] font-mono uppercase font-medium tracking-widerwith#22C55E/#EAB308 pulse/#EF44448px dots). - Bottom VersionStamp Footer matching
VersionStamp.tsxwith embedded documentation link.
- 2-Tab Layout (
- 1-Click Copy Node ID — added clipboard copy icon button with stateful
✓/COPIED!feedback next to Node IDs on both Target Sync Cards (SyncTab.tsx) and Companion Active Selection Cards, preserving clickable source hyperlinks (ID: {nodeId} ↗). - Penpot Live Canvas Selection — added
penpot.on('selectionchange')listener and 1s heartbeat polling inruntime.tsfor real-time canvas frame tracking.
Fixed
- FigJam → Penpot Export Relay Timeout — resolved command action name mismatch (
export-shapevsexport-frame) and forwardedshapeIdin companion relay dispatch, fixing 15s placement timeout during FigJam canvas imports. - Companion Log Clarity — updated Ably command logs to dynamically identify request origin (
FigJamvsBoard/Miro) and generalized expiration warnings. - Plugin Window Dimensions — increased companion window height in
figma-plugin/code.jsandpublic/penpot-companion-plugin.jsto650px(320px × 650px) to prevent vertical scrollbars on Settings and Sync tabs. - Typography Consistency — frame names now use system UI sans-serif
text-xs font-semibold text-text-page(12px), restricting monospace strictly to IDs, scale multipliers, and dimensions.
Added
- Design System Architecture Specifications — created
doc/design-system.mdcomprehensively covering core tokens, syntax highlighting palettes, typography rules, component standards (Buttons, Status Badges, Selectors, Headers, Footers), and Web Documentation portal patterns (/docscode blocks, GitHub callouts, tables, Mermaid diagrams). - Stateless MCP (SEP 2575) Architecture Roadmap — updated
doc/architecture/mcp-roadmap.mdwith specifications for serverless MCP server endpoints (/api/mcp), multi-MCP chaining architecture with Figma Desktop / Penpot MCPs, and Streamable HTTP transport patterns. - Canvas Metadata & Cross-Board Security Documentation — updated
SECURITY.mdwith explicit details on canvas widget metadata minimization (fileKey,nodeId), server-side OAuth ACL enforcement for copied elements, and data flow properties for compliance reviews.
[0.16.1] - 2026-08-07
Added
- FigJam App (M1) — the FigJam plugin now shows the SyncingBoard panel instead of a stub. When
figma.editorType === 'figjam', the plugin loads a hosted React app (route/figjam-mirror, moved from/figjam-pluginin 0.16.1 to bypass the Figma desktop's persistent embedded chunk cache) that reuses the Miro sidebar components (AppHeader, TabNav, Sync/Import/Settings tabs, BoardStatusFooter), driven by a newuseFigJamPluginhook over the pluginfigma.uipostMessage bridge. figma-plugin/code.jsFigJam command branch:figjam-place(create or in-place IMAGE-swap of a tracked Rectangle),figjam-list/figjam-state,figjam-place-result; tracked rectangles dedupe byfileKey|nodeIdviasetPluginData.figma-plugin/manifest.jsonnow declareseditorType: ["figma", "figjam"](api stays "1.0.0"); design vs FigJam runtime split (figma.editorType) incode.jsandui.html.public/figjam-companion-ui.html(static fallback UI) shipped alongside the React route.- Update all copies — the Sync toggle now sends
allCopiestofigjam-place; the plugin updates every instance of the key. Per-selection sync still targets only the selectednodeIds. - Keep canvas size —
preserveSizeis honored: the plugin skips resizing (FILL crop kept in FigJam by design) for in-place updates and new placements. - Scale & format now apply — the Sync card group controls call
figjam-set-meta, persisting per-instanceformat/scale(round-tripped through node summaries); the sync pass renders with the chosen format and scale.propagateextends group changes to sibling copies. - Penpot is real in FigJam — "Place on canvas" exports via the Penpot Companion relay (
export_shape) → image rect; "Detect Selection in Penpot App" uses the pairing relay (callRelay). All previously stubs. - Replace now targets the plugin's OWN selection — figjam-replace rewrites whatever nodes are selected at message time (tracked rectangles and foreign images) in place; no more duplicate placements or hitting the wrong copy. Plugin-side selection also removes the FigJam app/plugin id round-trip that could resolve to stale nodes.
- Render cache (90s TTL) — repeated Import/Replace/Sync of the same frame+scale+format reuse the last rendered data-URL instead of calling the Figma Render API every click: replacing the same frame 4x now costs 1 render, so the rate limit is no longer hit after a few replaces.
- Live Figma selection is now an opt-in toggle (OFF by default) in FigJam Settings — no relay quota usage until you enable it; the Detect button stays the explicit path.
- Live Figma selection toggle is disabled during rate-limit cooldown — it greys out with a "Paused" note and re-enables automatically (~20s).
- Live Figma selection is disabled on Community plans: the toggle is greyed out and hard-disabled when the Figma account is free/Community — or when the plan was never detected (Figma only reports X-Figma-Tier on rate-limit responses). No override exists: polling would burn Figma rate budget the Community tier cannot cover. Pro accounts (detected tier "Pro") get the toggle back automatically.
- "Place on Canvas" always creates a NEW copy — the same-key in-place update used to silently overwrite the previously placed copy, so doing it again looked like "replacing the last image".
- Replacing Figma component instances now really replaces — instances/locked artwork ignore fill writes; the plugin verifies the fill landed and otherwise swaps the node object at its own position (old component removed, new image placed at the same x/y/size).
- Figma call telemetry — Settings now show "Figma API usage (this session)": render calls + cache hits, plus the last rate-limit (X-Figma-Tier / limit type / Retry-After) when present.
- Dismiss button on the import card — an "x" clears the source (Figma or Penpot) so Place/Replace are not left "active" after the job is done.
- "Reset image cache" link on the FigJam Import tab — clears the 90s render cache so the next import/sync/replace renders fresh from Figma.
- Rolling-window rate counter + pre-call pacer — Figma REST limits are a rolling 60s window (Pro = 10 calls/min per developers.figma.com/docs/rest-api/rate-limits/): every request occupies a slot for 60s, then it is freed (count drops one-by-one, not all at once). Settings now shows live "window usage: N/10 in last 60s", and the client waits for a free slot before issuing renders/node-info calls, so normal use never trips 429s in the first place.
- Account/plan detection for the rate counter — Figma REST does not expose the plan via any API call; the only signal is response headers (X-RateLimit-Limit / X-Figma-Plan-Tier / X-Figma-Rate-Limit-Type, which can ride on ANY response or only on 429s). render-batch and node-info now forward ALL of these on every response, and the client auto-tunes its window limit to whatever Figma reports (falls back to 10/min). Settings gains: plan tier line, window-limit selector (Auto / 1..30 calls/min, persisted via localStorage), still showing live usage in the rolling window.
- Multi-frame sync in 1 call + 3-frame cap — Sync now sends all selected frames OF THE SAME Figma file in a SINGLE render request (nodeIds[]): a 3-frame sync of one file costs 1 call, not 3. Per Miro parity the sync also caps at 3 distinct frames per press (community rate-limit hygiene): extra frames wait for the next press, with an info message. Penpot flows unchanged (relay, not Figma-limited).
- Sync blocks above 3 distinct frames (Miro parity) - selecting 4+ different frames prevents the sync entirely with an explanatory error; no partial/silent skipping. Copies of the same frame do not count toward the cap.
- Sync cap = Miro parity (verified against code): Miro disables the Sync button AND shows the amber "Only 3 items can be synced at once / Deselect some to continue" banner when >3 groups are selected (SyncTab, gated by groupedItems.length). The FigJam app now uses the SAME widget and SAME disabled condition (the FigJam appMode gate was removed); the hook keeps the identical defensive throw with Miro exact wording, no partial syncs anywhere.
- Source links on Sync cards (Figma-derived) — the Node ID row is now the "open in source app" link (no separate icon, saves card space). Figma cards derive the deep link from
fileKey/nodeId(https://www.figma.com/file/{fileKey}/?node-id={nodeId}— a stable, official URL pattern the plugin can always supply, so every Figma card links out automatically). Penpot cards show a plain ID (the Penpot sandbox exposes no editor URL — see Removed). - Header tagline now reads "Stateless Design-Board Pipeline" on both the Miro and FigJam surfaces.
- Pairing ID card copy clarified — FigJam Settings say "link FigJam and Penpot" (Miro keeps "Miro and Penpot"), and both surfaces now note the ID is also required to detect your selection in the Figma app.
- Detect now shows live progress in the footer — pressing "Detect Selection" (Figma or Penpot, Miro and FigJam) emits an amber pulsing "Waiting for the Companion …" bar for the up-to-8s relay wait instead of leaving the previous status message stale.
Changed
- FigJam is PNG-only now — SVG renders are rasterized in-browser before placement anyway ("FigJam rejects SVG images"), so the SVG format option is removed on the FigJam surface (Import tab selector and Sync tab per-surface dropdown); the Propagate checkbox reads "Propagate scale to all copies" in FigJam (Miro keeps PNG/SVG).
- Settings (Miro and FigJam) no longer show the "Figma API usage (this session)" card — the 429 status message carries the actionable rate-limit info instead.
- Replace is selection-only again — Import → Replace Selected rewrites only the images currently selected on the canvas (the earlier copy-propagation experiment was reverted per user decision); the per-card Propagate toggle in Sync stays opt-in.
- FigJam undo note removed (parity) — FigJam allows Ctrl+Z on API-created nodes, so the "API syncs cannot be undone with Ctrl+Z" hint was dropped from SyncTab (the claim was only ever truthful for Miro, whose editor does not undo API changes; FigJam does).
- Auto-retry on Figma 429 removed (Miro parity): Miro never auto-retries — it errors, arms the countdown and lets the user press again. The app used to fire a second call after Retry-After (~2x quota burn per press); now it errors + cooldowns, exactly like Miro.
- Own-rate-limit message now honest: the earlier copy said "per-IP" — the same body error (rate_limit_exceeded) also zones from the per-pairing relay budgets. Now the app says "SyncingBoard own safety budget (own limiter) — wait Ns", and never touches Figma/Penpot states. Figma 429s still produce the countdown for the Figma path.
- Status messages show the frame NAME, not the opaque key: plugin results now echo payload.name (falling back to the meta-stored name, then the key); the FigJam app's "Synced/Updated" statuses use it, so users can verify the node in Figma by name instead of a key like njv4GQnbUZRmoo4Wz76G1p|754:64083.
- Settings "Figma API usage" block decluttered: removed the auto "unknown (Figma reports it on rate-limit responses)" plan-tier line, the window-limit selector and the cache explainer; now shows only renders/from-cache counters, "this minute: N/10 (rolling window)", the plan tier only when Figma actually reported it, and Figma-reported remaining/resets + cooldown + rate-limit lines only when present.
- 429 telemetry no longer freezes after success: rate-info (plan tier / limit type / retry-after) and the rateLimited flag are cleared on every successful Figma render (including a successful 429-retry), so Settings shows the real, current state instead of a stale "retry after 9s" line from an old attempt. Root cause of the hour-long apparent lock: a stale/invalid Figma token in the dev app kept getting 429s (re-authorizing fixed it — token/session layer, not the 10/min window).
- Session cooldown after ANY 429 / zero-remaining header - Figma limiter is multi-dimensional (per-file, burst, priority tiers), so a 429 can happen well under the 10/min token window. The app now enters a hard cooldown (Retry-After + 2s; 60s when X-RateLimit-Remaining: 0) during which no render starts; Settings shows "Figma cooldown until hh:mm:ss". Retries no longer consume a window slot (each sync shows its true call count).
- Rate proxies were hiding Figma budget: render-batch only forwarded rate headers on 429s. Now it forwards X-RateLimit-Limit/Remaining/Reset + X-Figma-* on EVERY response; Settings shows "Figma says: remaining N · resets HH:MM:SS" and a 0-remaining header engages cooldown until the ACTUAL reset instant (X-RateLimit-Reset). Root cause of "429 after 2 local calls": the client counted 2/10 but never saw Figma's real remaining budget (header discarded), and retry-after 9s indicates the limiter that fired is a shorter-window bucket than the documented 10/min rolling window.
- Our own gateway was masquerading as a Penpot/Figma limit: src/middleware.ts caps every /api/* request at 60/min per IP — Figma renders AND Penpot relay pulls share that same bucket. The reported "Penpot sync failed with rate_limit_exceeded without syncing a single image" was OUR middleware (body error rate_limit_exceeded), not Penpot, not Figma. Fixes: catch-all raised 60 -> 240 req/min; the app now distinguishes rate_limit_exceeded (own gateway; clear message, no Figma telemetry touched) from a genuine Figma 429 (cooldown + countdown).
- Penpot daily lockout — root cause and fix: the relay:export budget (2 exports/min, 20/day per pairing in rate-limit.ts) is what actually blocked Penpot syncs for a day: every Penpot import/sync/replace consumes one export, and the daily window (86 400 s) does NOT refill by waiting. Community defaults raised to 8/min and 100/day (relay:request 8/min, 60/hour, 200/day, responses 40/min); .env.example synced. The per-pairing budgets are still enforced — testing a whole day no longer trips them.
- Miro metadata write fixed: undefined values sanitized — an unconditional metadata field (and any undefined width/height) broke Miro REST validation ("Invalid value at "value" … received undefined"), blocking Penpot imports on Miro; MiroAdapter now drops undefined keys from the metadata object before setting it (url only ever written when present).
- Penpot manifest: version field removed — Penpot 2.17 RC5 rejects any non-default manifest version ("invalid manifest version" even for small ints); its plugins-runtime schema defines no version key at all and defaults to 1 when absent. The manifest now omits it, and plugin freshness is guaranteed by the
Cache-Control: no-storeheader on/penpot-*(vercel.json) plus Penpot re-fetching dev plugins per run. The earlier app-version encoding / git-count experiments are reverted.
Fixed
- FigJam-only polish round: "Keep canvas size" hint now says "Dimension and Crop locked."
- Scale now resizes the canvas object —
figjamPlacesizes the rect to the exported pixels (PNG already carries the render scale; SVG width/height × scale). 1× → design size, 2× → double, still crisp. - Crop position survives re-syncs — the previous IMAGE fill's
imageTransformis carried onto the new fill, so "Keep canvas size" no longer resets your crop. - SVG placement gets real dimensions — new
svgDimensions()parses width/height + viewBox out of the SVG data-URL so SVG imports are aspect-correct (were falling back to 240×160). - Penpot import actually imports — the SVG branch now base64-encodes the relay's SVG text into a
data:image/svg+xmlURL (was throwing "did not return an image"); the relay's real node name/width/height are applied; timeout is 45s with an "open the Penpot Companion" hint instead of a 2-minute silent hang. - Penpot channel is isolated — exports ride the
penpot:<pairing>Ably channel, so a Figma companion on the same Pairing ID can never answer a Penpot export (the "is it reading the Figma file?" case). - M3 destination relay-pull — "Detect Selection in Figma" in the FigJam app now pulls the active design-file selection over
figma:<pairing>(callRelay→ select), and the Figma design companion streams everyselectionchangelive so the FigJam app's Import card fills as you click around the Figma file (10s guard so a pasted link isn't immediately overwritten). The FigJam app subscribes as a subscribe-only Miro-role client — it never registers in the source presence set, so server-side companion detection is unaffected;subscribeRelayLive()keeps the Ably connection open while subscribed and releases it on unmount. - Session IDs without
crypto.randomUUID— the Figma/FigJam embedded WebView exposesgetRandomValuesbut notrandomUUID, which broke Figma AND Penpot detection and the M3 live subscription with "globalThis.crypto.randomUUID is not a function".generateSessionId()now falls back togetRandomValues→Math.randomwhile always producing a valid UUID v4. - SVG import/sync actually works in FigJam — createImageAsync rejects SVG data-URLs ("Image type is unsupported"), so SVG from Figma AND Penpot is now rasterized to PNG in the browser (canvas at natural size x scale) before placement; scale semantics stay identical to PNG (1x = design size, 2x = double, crisp).
- Replace works for Penpot too — Import "Replace Selected" no longer says "Miro-only": both Figma and Penpot sources rewrite the selected nodes via the new forceNodeIds on figjam-place.
- Replace works on foreign images — the FigJam selection summary now carries tracked + foreign node ids, so images placed by hand/other plugins can be selected and replaced (their fiddly data is rewritten to the new frame key).
- FigJam Sync button label — the action now reads "SYNC SELECTED" (shortened).
- FigJam panel height —
figma.ui.resize(390, 880)moved to just after showUI (calling it before the UI existed was a no-op), so the panel grows for real; the min-height CSS that was clipping the FigJam app footer was removed. - FigJam Sync is now fully selection-driven: cards list every selected tracked instance (duplicates count as the images selected — group badge
xN),figjam-placereceivesnodeIdsfor the selected instances and updates only those (a single selected duplicate no longer syncs all its copies), and the sync loop renders once per unique frame while applying to the selected instances only. - FigJam duplicates no longer drift:
figjamPlaceupdates every rectangle carrying the samefileKey|nodeIdin place, the the card dedupes by key (copies don't spawn extra rows — one frame = one row), and the sync loop paces 700ms between frames plus surfaces a friendly "Figma is rate-limiting" message on 429/rate_limit_exceeded(so the 4-attempt burst stops breaking the flow). figjamPlacePNG-dimension helper: the call site referenced a misspelledpngDataSize(vs definedpngDimensions) — the error surfaced asfigjam-place failed ('pngDataSize' is not defined); renamed the call.- FigJam app:
figjam-stateis now authoritative — an empty board report clearsselectedItems(the old keep-on-empty merge kept "Sync" acting as if a selection was still active when the board had nothing tracked). - Placed FigJam figures now match the source frame size:
figjamPlacedecodes the PNG's own IHDR dimensions (viafigma.base64Decode) and resizes the rectangle to the frame's aspect before the FILL swap — previously an updated rect kept its old size and FigJam's FILL cropped the image to the old rect ("using the previous rectangle as crop area"). - Swap-on-replace no longer leaves old artwork (fixes "old content overlapping the synced image"): Figma remove() a node, its children get reparented to the parent — so the swap fallback (unlocked/locked components) used to delete the wrapper but leave children floating over the new image. The plugin now recursively deletes the whole subtree (children-first) and places the replacement rect in the SAME parent (preserving frames/layers + z-order) instead of page root. Both branches fixed (sync in-place path + Replace). Plugin result now carries a swap flag so the UI says "Updated (node swapped)" instead of pretending an in-place fill update; re-import figma-plugin/code.js to apply.
- Replace on components: second root cause fixed — instance fill OVERRIDES sneak past the swap guard: writing fills to a component/instance often succeeds as an instance override, so fillImageMatches passed and the plugin kept the node in place — its children (inner shapes) stayed visible above the new image (the exact "still keeping the old content" report). Now nodeLooksLikeArtwork() forces the physical swap for COMPONENT, INSTANCE, or any node with children: the whole subtree is removed (instances are atomic — remove() deletes their content) and a clean rectangle replaces it in the same parent. Plain tracked rectangles (image-only, no children) keep the crop-preserving in-place path. Re-import figma-plugin/code.js.
- CI fix: changelog MDX builds again — a bullet contained a literal swap flag with brace syntax inline, which next-mdx-remote treats as a JSX expression (acorn failed compiling /docs/changelog, killing the build). Reworded; docs page prerenders again.
- FigJam import/replace buttons disable while placing —
importFigmaScreenin the FigJam app now enters the syncing state (like Penpot import and Replace), so Import/Replace buttons show PLACING… and disable while a Figma frame renders and places. - FigJam Replace button disables while processing — replace no longer accepts a second click mid-job (success/error/watchdog all release the busy state), matching Miro.
- Miro app never booted in hidden/headless iframes —
isInitModewas set insiderequestAnimationFrame; Miro's headless iframe is hidden (rAF paused) and React Strict Mode cleanup canceled it before firing, leavingisInitModenull forever: the page returned null,icon:clicknever registered, and Miro showed no toolbar icon. Now parsed synchronously fromlocation.searchon mount. - Penpot Companion: empty-selection results route over Ably — "None selected" answers no longer depend on the HTTP fallback (which 404s when the companion tab was loaded from a stale origin); detect now reports "No frame currently selected in Penpot." cleanly instead of timing out.
- Miro SDK loading — ancestry-gated global bootstrap (final): the previous attempt scoped miro.js to the /miro-plugin route with a plain defer tag — the Miro app still did not open. Official docs clarify why route-scoping is fragile: the app loads in MULTIPLE iframes whose URLs come from the app config (headless + panel + modal iframes all serve OUR app URL), so the SDK must exist on any page Miro can request, and only there. The layout now injects miro.js from the global layout, but ONLY when the page is embedded under a Miro origin — detection is
location.ancestorOrigins(any ancestor ending in miro.com / miro-app.io), withdocument.referrerfallback. That covers Miro headless+panel+modal of any configured URL, while FigJam (Figma webview ancestors), dashboard, docs and marketing tabs never receive the SDK — no SdkConnectionError anywhere outside Miro, no route config to keep in sync. - Sync-all no longer multiplies by selection size — with "Also update all board copies" on, selecting N copies of the same frame re-swept the board once per selected copy and queued every copy N times (e.g. 2 selected x 9 copies = 18 update calls instead of 9), blowing past the 10/min Miro image-update window. The copy sweep now runs once per unique (platform, fileKey, nodeId) — matching FigJam's entries-dedupe model.
- Miro Sync tab "Propagate format & scale" label — the label was written as the literal HTML entity
&inside a JS string expression, so the sidebar rendered "Propagate format & scale to all copies" verbatim (HTML entities only decode in JSX text children, not inside{'...'}expressions). Replaced with a plain&.
Removed
- Penpot URL feature (both Miro & FigJam) — user-driven revert within this release: a companion-driven Penpot source-link flow (detect auto-fill, manual URL input, per-file localStorage memory,
urlin metadata/adapter/plugin plumbing, "Open in Penpot ↗") was prototyped and removed because the Penpot plugin sandbox cannot expose the editor URL — auto-fill could only ever show stale or manually-pasted URLs for unrelated frames. Removed: the manual URL input, the "Open in Penpot" link, URL memory/recall, and all stored Penpoturlplumbing (types, adapters, plugin metadata, summaries); the companion no longer sends aurlfield. Figma-derived deep links remain (fileKey+nodeId pattern, see Added). Re-import the Penpot companion plugin once so it stops carrying the deadurlfield.
Internal
- Version bumped to
0.16.1; localyarn devshows a build id (v0.16.1.local-<git-sha>) that is never rendered in production bundles (inject-version.mjswritesBUILD,version.tssuppresses it when compiled withNODE_ENV=production). - New
GET /api/build-id(returnslocal-<sha>in dev,''in prod builds) lets the static Figma/Penpot companions append the build id to their badge at runtime; the Miro/FigJam footers use a client-onlyVersionStamp(no hydration mismatch). - GA/consent is fully suppressed on the FigJam plugin route (
/figjam-plugin): the g-tag/sw/gtag scripts never load and the cookie banner is hidden (extended the same iframe-route guard used by/miro-plugin). - FigJam panel hides the Miro connection dot (AppHeader
hideMiro) and the "Miro REST Status" row (SettingsTabhideMiro); Penpot source-sync is kept as-is. - FigJam mode now shows the same "Preview host (optional)" footer as Figma mode (self-host/testing override), instead of hiding it.
- Preview-host allow-list extended with
*.protokoba.com/protokoba.com(matches the manifestdevAllowedDomains), so a dev host no longer falls back to production and 404s the FigJam route. - FigJam "Detect selection" no longer reads FigJam nodes (it is the destination): it explains the selection lives in Figma (run the plugin there / paste a link).
- Import paste feedback: a missing
?node-id=(or invalid link) now shows an inline error under the input instead of failing silently. - FigJam link import: uses the shared
parseFigmaUrl(convertsnode-id=754-64083→754:64083, the format Figma's REST API requires). Fixes the wrong fallback name ("Pasted Screen") and "Figma render returned no image for the node" when placing. - FigJam placement: 25s watchdog prevents the UI from staying stuck in "Rendering…" when the plugin never confirms;
createImageAsyncfailures now report the real error instead of a generic hint. - The Figma/FigJam panel iframe URL now carries a unique
&_t=<timestamp>cache-buster: route chunk names stay identical across dev-server recompiles, and the Figma desktop app can cache the old bundle under the same URL (diagnosed via the browser-vs-panel build-id split — the desktop panel kept showinglocal-63c65f8while the server servedlocal-a947486). Even the busted reload still showed the old build, proving chunk caches are page-agnostic — so dev mode ALSO sendsCache-Control: no-storefor/_next/static/chunks/:*(next.config.ts), forcing a real chunk refetch every Apply/boot. - Archived the previous run of
figjam-plugin/ui.htmlFigJam stub (replaced by the hosted route). .gitignorenow excludes local build logs + scratch scripts.- EOL normalization of
useFigJamPlugin.tsandusePenpotImporter.ts— mixed CRLF/LF line endings collapsed to pure LF (large diffs shrank to a few lines, keeping history reviewable) during the URL-feature churn in this release. - Removed the dead
src/lib/sync/penpotUrlParser.ts+ its test (unreferenced since the Penpot URL removal above). - Dependency resolutions made Yarn-classic-compatible (Vercel build fix) — the descriptor-scoped
brace-expansion@npm:^1.1.7/@npm:^5.0.5resolution keys are valid Yarn Berry syntax but breakyarn installon Vercel (which runs Yarn classic and re-resolves the tree from scratch:DEP0170 The URL npm:^1.1.7@npm:1.1.18 is invalid→registry.yarnpkg.com/1.1.18: Not found). The committed Yarn-4 lockfile already pins the patched versions (brace-expansion 1.1.18 + 5.0.9), so the keys are removed entirely: Yarn classic's fresh resolution also picks the latest patched versions (both satisfy the minimatch ranges), andyarn npm auditstays at 0. <VideoTabs>single-video mode — when only one offigma/penpotis provided, the component renders that video directly without the Figma/Penpot tab bar; used for the new "SyncingBoard showcase" hero video on/docs/features(placed before Getting Started). URL normalized to the site'syoutube-nocookieprivacy pattern.
[0.16.0] - 2026-08-07
Added
- Shared Target Layer (Architecture): Introduced a target-agnostic whiteboard core so FigJam can mirror Miro (and future Mural / Whiteboard / Excalidraw / tldraw targets can follow) instead of each target re-implementing placement/update/adopt logic.
- Target-agnostic core extracted into
src/lib/sync/:companionRelayClient,relayAbly,useRelayStatus,pairingId(getOrCreatePairingId), and the Figma/Penpot URL parsers are now pure platform-agnostic modules — moved fromsrc/app/miro-plugin/, Miro-free, zero logic change (11 import sites updated). TargetAdapterseam (src/lib/sync/targetTypes.ts): strictly-typedNodeUpdate,AdoptMeta,FrameSelection,FramePlacement(sourceUrl, renderWidth, metadata passthrough),TrackedNode(incl.metadataSaved/metadataError),TargetCapabilities, and theTargetAdapterinterface — one contract every target implements: selection read, create-or-update placement, in-place update, adopt/re-target, title re-assert, geometry read, pairing host, and selection trigger.MiroAdapter(src/app/miro-plugin/MiroAdapter.ts): full Miro Web SDK adapter implementing the shared seam above a minimal constructor-injected structural board (unit-testable; noany). 8 unit tests (MiroAdapter.test.ts).
Changed
- Miro plugin rerouted through the adapter (behavior-preserving, no feature change): placement (
useFigmaImporter,usePenpotImporter), the sync-all update loop (useMiroSyncSTEP-2 viaupdateNode), and replace-selected retarget/adopt (useMiroPluginviaadopt+updateTitle) now delegatewindow.miro.*calls tonew MiroAdapter(miro.board). Render batch,update-imagePATCH, status/rate-limit/cooldown orchestration stay in the hooks — output unchanged, Miro tests stay green. - Selection lifecycle stays target-specific (
useMiroSelection): the headless boot/retry, the non-SyncingBoardhasAnyImageadoption signal, naturalwidth, andBroadcastChannelbroadcast are plugin lifecycle, intentionally not tunneled through the adapter. Each target exposes its owngetSelection()/selectionTrigger()contract (FigJamFigJamAdapter.getSelection()will mirror Miro's normalization).
Internal
- FigJam Phase-0 groundwork (equivalent-to-Miro target): re-validated the Figma Plugin API against v1 spec — manifest stays
api: "1.0.0"withfigjamadded toeditorType(no API version bump); gated APIs (createSticky / createShapeWithText / createConnector / createCodeBlock / createGif / createTable /figma.timer) surfaced;createImageAsyncand IMAGE fills are ungated. Pairing model confirmed from code: the target owns/generates its pairing key viagetOrCreatePairingId()and the source companion (Figma/Penpot) joins — so the FigJam target generates its ownsb_key. v1 FigJam runs cloud-tiered (Ably + Upstash chunked), no local bridge for the free tier (community Tauri bridge deferred).
[0.15.3] - 2026-08-05
Fixed
- Local Dev Tunnel Domains & Companion Transfer Banner:
- Added wildcard tunnel domains (
*.protokoba.com,*.trycloudflare.com) toallowedDevOriginsinnext.config.tsso Next.js 16 allows requests from custom development tunnels. - Added
https://*.protokoba.comandhttps://protokoba.comtodevAllowedDomainsinfigma-plugin/manifest.jsonso Figma plugin's CSP permits embedding custom tunnel hosts in the companion iframe. - Added
yarn localscript (scripts/start-local-tunnel.mjs) to automate launchingyarn devandcloudflared tunnel run syncingboard-devin a single command. - Companion Transfer Banner Auto-Dismiss: Fixed
figma-companion-ui.htmlandpenpot-companion-ui.htmlso the amber"Companion Active in Another Tab"banner (transfer-card) automatically hides when a standby companion tab successfully connects after the primary tab closes.
- Added wildcard tunnel domains (
Fixed
- Dependabot Security Upgrades (Next.js, PostCSS, Tar & Cargo):
- Upgraded
nextandeslint-config-nextto16.2.11(resolves Next.js App Router advisories GHSA-6gpp-xcg3-4w24, GHSA-m99w-x7hq-7vfj, GHSA-89xv-2m56-2m9x, GHSA-p9j2-gv94-2wf4, etc.). - Added Yarn Berry package resolutions in
package.jsonforpostcss(^8.5.3) andtar(^7.5.0) to purge transitivepostcss@8.4.31(GHSA-7fh5-64p2-3v2j ReDoS) andnode-tarstack-overflow vulnerabilities. - Updated 75 Cargo crates in
tauri-bridge/src-tauri/Cargo.lockto latest secure patch versions (resolvingglib,tokio,hyper, andrustlsRust advisories).
- Upgraded
Changed
- Node 20 Environment Lock: Added
.nvmrc(20.20.0) andpackage.jsonenginesconstraint ("node": "^20.0.0"). - Next.js
workStoreResolution Verified: Verified clean Turbopack pre-rendering and build exit 0 under both Node 20 and Node 24 withnext@16.2.11.
[0.15.2] - 2026-08-04
Added
- Companion Token Cap (180 Max) & 20-Socket Miro Reserve: Introduced
RATE_LIMIT_COMMUNITY_MAX_COMPANION_TOKENS(default 180) to hard-cap active companion Ably tokens in Redis (relay:active_companion_tokens). Reserves a permanent 20-socket floor for Miro detectors, ensuring companion tab proliferation can never starve Miro sidebars. - Active-Pair Priority & Orphan Eviction: When the 180 companion token ceiling is reached, companions paired with active Miro boards receive priority issuance. Orphan standby companions (companion tabs with no active Miro lease) are evicted starting with the oldest token and broadcast
{ event: 'companion_evicted' }to render"Standby (Slot granted to active sync)". - Companion 1-Tab-Per-Pairing & Transfer UX: Added
/api/relay/companion/sessionendpoint (releaseandtransfer) andrelay:companion_session:{pairingId}Redis keys. Second companion tabs for the same pairing receive{ companionConflict: true }and render an amber"Companion Active in Another Tab"banner with a 1-click "Transfer Connection" button. - Client-Side Ably Token Cache (R5): Introduced 2-hour TTL client-side Ably token caching in
src/lib/ablyTokenCache.ts. Eliminates redundant/api/ably/tokenHTTP roundtrips during active sessions. - Penpot Inline SVG Exports over Ably (R2): SVG exports with compact payloads (serialized JSON < 12KB) now stream directly over Ably channels via
resultmessages instead of the HTTP + Redis path; PNG/base64 and large payloads keep the Redis path.
Changed
- Optimized Relay Status Polling (R1): Removed 30-second blind status polling intervals from
useRelayStatus. Status is now polled on-demand during connection state transitions and gated by a 10-second serverless Redis deduplication cache (SET NX EX). - Removed Sync-Poll Loop (R4): Removed the legacy 350ms Upstash Redis GET polling loop in
/api/relay/request/route.ts. All callers now use async pub/sub transport. - Lua-Batched Rate-Limit Windows (R3): Multi-window endpoints (relay 5/min + 30/hour + 100/day) now batch all windows in a single Redis EVAL via
checkManywhen the Redis backend is active (1 command instead of N), with a fallback to independent checks.
Fixed (Figma Community review - post-release)
- Figma Plugin Network Access: The production manifest
allowedDomainsnow includes the apexhttps://syncingboard.com(the*.syncingboard.comwildcard only covers subdomains). Dev-only origins (gitpod.io,github.dev) and the maintainer's personalluiskobayashi.comdomain were removed from the plugin's network access;devAllowedDomainskeeps only the standard localhost dev ports. The reviewer-facing "This content is blocked. Contact the site owner to fix the issue." error was Figma's plugin-UIframe-srcCSP refusing the companion iframe when the submitted manifest lacked the production domain. - Zero-Setup Plugin UI: Removed the custom-host Configure panel and the
clientStoragehost override fromfigma-plugin/ui.html+code.js; the plugin now always loads the companion fromhttps://www.syncingboard.com. Self-hosters edit theDEFAULT_HOSTconstant inui.htmland list their domain inallowedDomains. Also stripped debugconsole.lognoise fromcode.jsfor the submission artifact.
Changed (post-release)
- Preview host restored (optional): the Figma plugin's footer now has a collapsed "Preview host (optional)" field for testing against a staging/tunnel host or self-hosting before production — persisted per plugin instance via
clientStorage. Empty = production default (https://www.syncingboard.com); the host must be listed inallowedDomains(devAllowedDomainsfor dev plugins) due to the frame-src CSP. - Unlimited pool mode:
RATE_LIMIT_COMMUNITY_MAX_RELAY_SESSIONSandRATE_LIMIT_COMMUNITY_MAX_COMPANION_TOKENSnow accept0for an unlimited pool (previously0/invalid input fell back to the default). Unlimited pools report the enforcement ceiling and never derive afullrelay status; the Redis Lua caps are raised so thecount >= limitguard never trips. The real ceiling remains your Ably plan's connection limit.
[0.15.1] - 2026-08-03
Added
- 1 Active Board Per Miro User (Session Binding): The relay now binds each Miro user to a single board via
relay:user_board:{sha256(miro.currentUser.id)}(30-minute TTL refreshed on every heartbeat). A user holding a lease on board A who starts syncing on board B is detected at token issuance — the new board receives200 { conflict, activeBoardId }instead of silently double-holding capacity. Guests with OAuth are first-class users; users without OAuth cannot sync and never hold a session (connections are lazy, so no server-side auth gate is needed). - One-Click Session Transfer UX: Both boards (the current holder and the new board) show an amber "Transfer Session" card. One click repoints the binding via the Lua
transferaction, frees the previous holder, then re-establishes the Ably client under the same session — no token re-issuance round-trip. The old board's next heartbeat reports the conflict and both sides converge on the same binding. The transfer button is gated by a 7-second cooldown. - Tauri Local Transport Indicator (C2): The Rust bridge
/healthendpoint now returns{ status, figmaConnected, miroConnected }with real per-service connection tracking (service_connections). When the desktop bridge is active and Figma is connected, the sidebar shows a cyan "Local Transport (0/40 slots used)" card — the desktop relay bypasses the cloud pool, so the UI reflects actual slot usage. - Pure Decision Tables (M5):
planAcquire/planTransferextract the binding rules (renew / grant / conflict / full) into exported pure functions mirrored by the Redis Lua script — 7 new unit tests cover renew, conflict, grant, full, and transfer paths without a Redis instance.
Changed
- Conflict at issuance, not after connect:
/api/ably/tokendetects the cross-board conflict before issuing a token (200 { error: "relay_conflict" }), distinct from capacity-full429+Retry-After;/api/relay/status?userIdHash=&boardId=returnsuserConflict+activeBoardIdso the banner refetch converges instantly. - Legacy clients unchanged: sessions without a user identity keep the previous pool-only semantics (heartbeat / acquire / release work exactly as before) — 0.14.1 compatibility preserved.
[0.15.0] - 2026-08-03
Added
- Community Active Slot Counter (
/api/relay/status): New public endpoint reports live relay capacity —{ activeSessions, maxSessions, globalSyncsToday, maxGlobalSyncs, status }— with status levelsavailable/high_load(≥75% of ceiling) /full(at ceiling). Polled by the Miro sidebar every 30 seconds. - Graceful Queue UX (Sync tab banner): Live capacity banner with green (
Community Relay: n/40 slots), amber (High Demand), and red (Capacity Full) states. When full, a manual "Check again" button replaces auto-retry, gated by a 7-second cooldown with a quiet countdown, so users cannot hammer retry. - Target/Source-Agnostic Relay Sessions: Session leases renamed from Miro-specific (
relay:miro:sessions,acquireMiroRelaySession) to generic relay naming (relay:sessions,acquireRelaySession) so one capacity pool covers Figma/Penpot → Miro today and FigJam/Mural later. New envRATE_LIMIT_COMMUNITY_MAX_RELAY_SESSIONS(default40); legacy aliasRATE_LIMIT_COMMUNITY_MAX_MIRO_RELAY_SESSIONSstill honored. - Global Daily Syncs Display Counter: Best-effort
global_syncs_todaycounter (24h TTL) incremented alongside the daily global backstop, so the status endpoint can surface community-wide usage. - Capacity Failure Messaging: Ably auth failures caused by
relay_capacity_reachednow surface a clear "Community relay is at full capacity" message instead of a raw connection error. - Unit Tests:
deriveRelayStatusLevelboundary coverage (available/high_load/full at 40- and 60-slot ceilings).
Changed
- No paid upsell in queue UX: The full-capacity hint points to the upcoming free desktop (Tauri) tier instead of a paid plan.
[0.14.1] - 2026-08-02
Security
- Community Rate-Limit Enforcement: Restored the documented Community defaults (Figma
5/min, Miro image updates10/min), activated Figma50/dayand relay5/min + 30/hour + 100/daywindows, and excluded OAuth polling, Ably token issuance, node-info, and relay bookkeeping from the shared render/update resource budget. - OAuth Refresh Protection: Added
RATE_LIMIT_COMMUNITY_OAUTH_REFRESH_PER_MIN(default3) to rate-limit/api/oauth/refreshby a one-way refresh-token fingerprint without storing raw refresh tokens. - Ably Ghost-Connection Recovery: Figma Companion now uses Ably
authUrltoken renewal and handles terminal connection states; the Miro relay client now invalidates terminal clients, closes after 60 seconds idle, and releases its socket onpagehide. This reduces persistent idle connection pressure on the Community Ably plan. - Relay Integrity & Community Capacity: Export-result submissions now require a Redis-backed
requestId → pairingIdbinding and are rate-keyed by pairing instead of unique request ID. Miro-only Ably tokens now use a Redis sorted-set lease, renewed every 15 minutes and released on idle/page-exit, with a default Community ceiling of 40 concurrent Miro relay sessions. - Community 429 Cooldown UX: The Sync tab now distinguishes SyncingBoard
rate_limit_exceededresponses from Figma provider 429s, readsRetry-After/reset metadata, disables repeated sync attempts, and displays a live Community cooldown countdown. - Relay Ghost-Path Hygiene: Both companion UIs now clear their connect timeouts, bound unanswered selection/export requests to per-action timeouts, and leave presence on page exit. Relay result retention increased from 45 to 180 seconds, while the Miro relay client retries transient result 404s before failing.
- OAuth Callback Rate Limiting:
/api/oauth/figma/callbackand/api/oauth/miro/callbackare now wrapped by the rate limiter (20/min per client IP), closing the last unwrapped token-exchange path./api/oauth/refreshwas already limited per refresh-token hash. - Relay Export Sub-Budget: Heavy Penpot/Figma relay exports now draw from a dedicated per-pairing budget (2/min, 20/day) on top of the general relay limits, separating the costlier Ably + Redis + payload path from lightweight selections.
- Figma node-info Error Transparency:
node-infonow propagates upstream Figma 401/403/429 responses (withRetry-After/plan-tier metadata) instead of masking every failure as{ name: "Pasted Screen" }; only genuine 404s map to the fallback name. Clients already degrade gracefully on non-OK responses. - Provider Rate-Limit Awareness: Miro 429 responses in
update-imagenow honorRetry-After(capped at 10s) during geometry-retry backoff instead of a fixed 800ms delay, and the image upload surfacesretryAftermetadata. - Rate-Limit Header Transparency: Successful responses from limited endpoints now include
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resetso clients can throttle proactively instead of discovering limits from a 429. - Identifier Hygiene: The
figma:renderlimiter no longer accepts?token=query values as identifiers — tokens are Authorization-header-only, matching the route contract. - Companion Readiness Signal: Figma/Penpot companions enter Ably presence with a
readyflag, and the relay server treats only ready members as online — distinguishing "present" from "ready to handle commands". Presence staleness after an abrupt disconnect (~2 min) remains documented as a free-tier residual. - Unenforced Knobs Removed:
RATE_LIMIT_COMMUNITY_GLOBAL_BANDWIDTH_MB_PER_DAYandRATE_LIMIT_COMMUNITY_MAX_COMPANION_PAIRSwere advertised but never enforced; both are removed from code configuration,.env.example, and documentation. Community capacity control is the 40-session Miro relay lease.
Changed
- OAuth Polling Slowed: Popup completion polling in
useAuthTokensdropped from 1.5s to 4s intervals (~75 polls per 5-minute attempt instead of ~200), cutting avoidable edge/Redis traffic. - node-info Quota Protection & Manual Card Refresh: Removed automatic
node-infoAPI calls from the sync loop inuseMiroSyncto preserve daily Figma API rate-limit quotas during routine syncs. Added a manual ↻ refresh button to each canvas screen card in the Sync tab so users can refresh frame names on demand when modified in Figma. - Global Backstop Reset Accuracy: Updated
checkGlobalDailyBackstopinrate-limit.tsto surface the backend Redis window reset timestamp directly, ensuring accurate client-side cooldown countdown timers inuseMiroSync. - Cooldown Interval Stability: Wrapped
setSyncStatuswithuseRefinuseMiroSyncso parent re-renders do not recreate the 1-second cooldown timer interval.
[0.14.0] - 2026-08-01
Fixed
-
Keep Canvas Size — Geometry Preservation Rewrite: Fixed the "Preserve widget size" feature (renamed "Keep canvas size") which was broken since its introduction in
0.10.0. The root causes were: (1) the geometry PATCH used the wrong endpoint (/items/{id}with a JSON body) instead of the image-specific endpoint (/images/{id}with multipart form data); (2) thepreserveSize=truepath skipped the geometry write entirely, relying on an undocumentedstyle.fit: 'contain'field that the Miro API ignores; (3) both paths were therefore always leaving the widget at Miro's auto-calculated size.New implementation: before the binary upload, the server reads the current canvas
geometry.widthvia a snapshot GET. After the upload, it re-applies the target width via the correct image endpoint using a multipart form with up to 3 retry attempts, each followed by a GET verification to confirm the width stuck. Both paths now work correctly:preserveSize=truerestores the pre-upload snapshot width;preserveSize=falseapplies the client-provided natural Figma/Penpot width. -
UX Honesty — Crop Platform Limitation: Renamed "Preserve widget size" to "Keep canvas size" and added a sub-note in all three occurrences (SyncTab, ImportTab Figma, ImportTab Penpot): "Size locked. Crop resets — Miro API limitation." Miro does not expose crop state (mask coordinates) in its REST API or Web SDK v2, so crop cannot be preserved programmatically. This is a hard platform ceiling, not a code limitation.
Added
- Undo (Ctrl+Z) Platform Notice & FAQ: Added inline UI micro-notes (
API syncs cannot be undone with Ctrl+Z) under primary action buttons inSyncTabandImportTab, plus a dedicated entry indoc/faq.mdunder Technical Design & Constraints explaining that Miro API updates bypass client-side undo history. - Figma Companion Setup & Host Settings Documentation: Updated
doc/setup.mdto explicitly document the first-time "Pair Figma Design File" prompt steps in the Figma Companion plugin, and updated self-hosting configuration instructions to reference the exact SyncingBoard Host Settings UI title.
[0.13.6] - 2026-07-29
Added
- Interactive Quick Start Guide & Vercel 1-Click Deploy: Implemented
QuickStartSection.tsxon/docswith tabbed guides for Community (Cloud Hosted) vs Self-Hosted deployment. Added direct 1-click Miro install URL button (Install to Miro Team ↗) and official 1-click Vercel Deploy badges (Deploy with Vercel) across/docs,README.md, anddoc/setup.md. - Figma Title Signature Standardization: Standardized Figma widget title metadata signature from
[SyncingBoard|...]to[FigmaSync|...]for 1:1 naming symmetry with Penpot's[PenpotSync|...]. - Plugin Manifest Network Scoping: Scoped Figma companion plugin network permissions in
figma-plugin/manifest.jsonby removing the overly broad*.vercel.appwildcard domain. - Documentation & FAQ Comprehensive Audit: Updated
README.mdanddoc/faq.mdto clarify Figma Companion plugin scope (only needed for selection detection), updated fileKey resolution FAQ to explain multi-layered fallback, updated image download filename phrasing, replaced placeholder video embeds indoc/features.mdwith "Coming Soon" cards, updatedVideoTabGroupHydratorto support[class*='aspect-video']placeholder containers so Figma/Penpot tabs hydrate properly, linked official Security Policy (/docs/security) and Privacy Policy (/docs/privacy), added platform API limits FAQ entry with a link to Security, Rate Limits & Quotas Architecture, fixed rehype doc link resolution for root markdown files, clarified enterprise commercial licensing availability (directing tocontact@syncingboard.com), and added FAQ entries covering frame renaming, one-way sync safeguards, uninstallation persistence, and private Figma workspace support.
Security
- Header-Only Figma Token Transmission: Removed
?token=and?Authorization=query-parameter fallbacks from/api/figma/renderto prevent OAuth tokens from appearing in HTTP logs, edge logs, or browser history. - Strict Miro CSRF State Enforcement: Removed redundant
MIRO_ALLOW_DIRECT_INSTALL_NO_STATEenvironment override to ensure state validation is consistently enforced for standard OAuth flows while retaining automatic direct link install detection. - Strict Parameter Format Validation: Added regex format validation for
boardIdanditemIdin/api/miro/update-imageto prevent path traversal in Miro API URLs, andpairingIdslug validation in/api/ably/tokenGET/POST endpoints. - Relay Request Body Sanitization: Replaced unsafe type assertion in
/api/relay/requestwith explicit object structure validation. - Upstash Redis Request Timeout: Added a 10-second
AbortControllertimeout to all Redis REST requests insrc/lib/relayRedis.tsto prevent function stalls on transient network latency. - OAuth Callback Security Headers: Injected
Content-Security-PolicyandX-Content-Type-Options: nosniffheaders across Figma and Miro HTML callback responses.
Fixed
- Null-Safe HTML Entity Decoding: Made
decodeHtmlEntitiesnull-safe and updateduseFigmaImporter.tsinitialfigmaNodeInfo.namestate fromnullto'Loading...', preventingTypeErroriframe crashes ("The page couldn't load") when pasting Figma links into the Miro plugin. EncodednodeIdin/api/figma/node-info. - Figma Render Route Test Suite: Updated
src/app/api/figma/render/route.test.tsto explicitly assert HTTP 401 rejection when tokens are passed via URL query parameters. - Security Policy Supported Versions: Updated
SECURITY.mdsupported versions matrix to reflect active security patching for0.13.x.
[0.13.5] - 2026-07-27
Added
- Full-text interactive search engine: Implemented relevancy scoring (+100 Title, +50 Heading, +30 Description, +10 Body), category hierarchy weighting, historical archive demotion (-60),
<mark>search term highlighting, section deep-linking (#heading-id), andCmd+K/Ctrl+Kkeyboard shortcut. - Dedicated Environment Variables Documentation: Created
doc/environment-variables.mdwith complete key reference, rate-limiting overrides, and.env.exampletemplates, indexed under System Design & Adapter Modules. - Sticky Top Bar & TOC Offset: Added sticky glassmorphism header (
sticky top-0 z-50 bg-bg-page/80 backdrop-blur-md) with TOC top offset increased totop-28(112px) to eliminate text overlap under the top bar. - Homepage Action Bar: Placed
[ Privacy ],[ Cookie Settings ], and[ Theme ]in top-right action bar with uniform button heights.
Changed
- Setup Guide Reorganization: Consolidated all HTTPS tunneling options (
cloudflared,ngrok,localtunnel), local Miro Developer App settings, local Figma Companion import, local Penpot Companion import, and local Figma OAuth App config into Section 6 (## Local Development). - Heading Numbering Cleanup: Removed numerical prefixes from section headers across
README.md,doc/setup.md, anddoc/environment-variables.mdfor consistent document styling.
Fixed
- Case-Insensitive Doc Routes: Normalized slug lookups in
getDocBySlug()so uppercase URL routes like/docs/LICENSEand/docs/licenseresolve 100% case-insensitively. - In-Page Filter Removal: Removed stale
totalResultsReferenceError and in-page card filtering fromDocsIndexClient.tsxso all category cards remain 100% visible while searching. - Infrastructure & Cloud Limits: Documented Vercel (4.5MB payload limit), Upstash Redis (10k req/day), and Ably Realtime (200k msgs/month) quotas in
doc/architecture/security-and-limits.mdwith official documentation links. - Figma & Penpot Architecture Specs: Updated
doc/architecture/sources.mdto clarify web/desktop selection relay support, Figma Companion scope, andpenpot.openPage()API definition.
[0.13.4] - 2026-07-27
Added
- Root-level documentation indexing: Added scanner support for
README.md,CONTRIBUTING.md, andSECURITY.mdinsrc/lib/docs.ts, exposing them as public/docs/contributingand/docs/securitypages. - Footer cookie settings button: Created reusable
<CookieSettingsButton />client component allowing users to reopen cookie preferences at any time from footers across public pages. - Reference documentation cards: Displaying 6 full-featured cards (Changelog, Privacy Policy, License & CLA, FAQ, Contribution Guidelines & CLA, and Security Policy) under the Reference section on
/docs.
Changed
- Documentation hub layout & typography: Upgraded
/docswith wide featured cards, cyan/blue outline borders (border-accent/40), and enhanced section category headers (text-xl md:text-2xl font-black). - Markdown style compliance: Removed emojis across all markdown documentation files and documentation components in alignment with project design system standards.
Fixed
- Root doc path resolution bug: Resolved
ENOENTerror ingetDocBySlug()by adding explicitROOT_DOCSset checking for root-level markdown files vsdoc/sub-directories. - Case-insensitive doc slug generation: Converted generated doc slugs to lowercase in
filenameToSlug()soCHANGELOG.mdcorrectly maps to/docs/changelog. - Dual-context markdown link resolution: Configured relative markdown links (
./doc/setup.md,./doc/architecture.md,./LICENSE) to resolve natively on GitHub and internally on the webpage. - Downloaded Miro image asset naming: Background binary
Fileheader registration (/api/miro/update-image) ensures right-click image downloads in Miro retain original frame titles (Frame Name.png).
[0.13.3] - 2026-07-26
Added
- Privacy policy page: New
/docs/privacydocumenting all transient operational data (rate limit IP counters, 45s Redis relay buffers, 5min OAuth states), their legal bases under GDPR, and user rights. Landing page footer now links to it. - Architecture Spec Audit & Remediation: Updated
doc/architecture.mdto align withv0.13.3implementation status — clarifying MCP Client/Server planned status, documenting token-hash rate limiting (tok:sha256(token)), geometry preservation (preserveSize), widget adoption/retargeting (replaceSelectedWidget), 300s Redis SETEX OAuth state handshake (/api/oauth/store), 16-char secure pairing ID masking/rotation (pairingId.ts), and HTML entity title sanitization (decodeHtmlEntities).
Changed
- Default scale 1x: All scale defaults changed from 2x to 1x across 6 files — initial states, localStorage fallbacks, import parameter defaults, selection fallbacks, sync fallbacks, and companion relay client. New imports and syncs now default to 1x resolution.
- Landing page footer: Updated from "Zero data stored on server" to "Your designs never leave your tools. No accounts. No permanent storage." — accurate about no design data or permanent storage, while transparent about transient operational data (now documented on
/docs/privacy).
Fixed
- Widget title on Place on Canvas: Both Figma and Penpot import flows now re-assert the widget title via Miro SDK after the background
update-imagePATCH completes — matching the pattern already used in sync and replace flows. This prevents Miro's server-side HTML encoding from overwriting the decoded frame name after the PATCH response.
[0.13.2] - 2026-07-25
Security
- Escaped dynamic OAuth callback HTML error messages in both Figma and Miro callback routes to prevent reflected XSS (
escapeHtmlsanitization for query/provider error strings). - Hardened
/api/oauth/storewith strict state/token payload validation and endpoint-level rate limits (oauth:store:get,oauth:store:post). - Added overwrite protection for OAuth state cache keys using Redis
SET ... NX EX 300semantics to prevent token-poison race overwrite. - Added rate limiting and requestId format validation to
/api/relay/response. - Added strict input validation for
/api/figma/render-batch(fileKey,nodeIds,format,scale) before proxying requests. - Tightened Miro callback state policy with controlled direct-install bypass behind
MIRO_ALLOW_DIRECT_INSTALL_NO_STATE=true.
Fixed
- Sync metadata integrity:
handleGroupSettingChangenow merges updates into existingsyncingboardmetadata instead of overwriting the object, preservingfileKey/nodeId/nodeName/platform/width/height. - Post-sync metadata completeness:
useMiroSyncnow persistsfileKey,nodeId, andnodeNamein metadata updates so fallback selection parsing remains reliable when titles are edited. - Rate-limit identifier correctness:
miro:update-imagenow keys limits by Authorization bearer token instead of attempting to read a body token that no longer exists. - HTML entity decode for frame names: Names containing HTML entities (e.g.
Expanded=True) now display correctly as their actual characters (Expanded=True). Createdsrc/lib/decodeHtmlEntities.tsand applied at all name entry points — relay response (companionRelayClient.ts), Penpot selection/export (usePenpotImporter.ts), Figma REST API and companion selection (useFigmaImporter.ts), sync name cache (useMiroSync.ts), and display components (SyncTab.tsx,ImportTab.tsx).
Added
- Pairing ID masking with eye toggle: All three Pairing ID fields (Miro plugin Settings, Figma companion, Penpot companion) now default to hidden (
type="password", shown as********) with an eye icon button to reveal. Protects against screen recording and shoulder surfing. - Pairing ID rotation: Added
REGENERATEbutton in Miro plugin Settings that generates a new randomsb_xxxID viarotatePairingId()insrc/lib/pairingId.ts, overwriting the stored value. Existing companion connections will need the new ID. - Documentation of rate limits, batch limit, and scale cap: Added default rate limit table (9 endpoints with env variable names), batch size limit (3 unique images), and community scale cap (1x/2x) to
doc/faq.mdanddoc/architecture.md.
Changed
- Miro plugin panel decomposition: Split
src/app/miro-plugin/page.tsxmonolith into focused components (AppHeader,TabNav,SyncTab,ImportTab,SettingsTab,BoardStatusFooter, shared types), reducingpage.tsxfrom ~970 lines to ~319 lines. - Selection grouping performance: Replaced repeated
getGroupedItems()render calls with memoized grouped state. - Pairing ID source of truth: Moved pairing ID generation into
src/lib/pairingId.tsand reused it across panel + relay client, fixing the empty-string localStorage edge case. - Analytics deduplication: Consolidated duplicated
trackEventimplementations intosrc/lib/analytics.ts. - Removed dead verify route: Deleted unused
/api/figma/verifyendpoint. - Removed production debug noise: Cleared
console.logdebug traces frompublic/penpot-companion-plugin.js.
[0.13.1] - 2026-07-25
Fixed
- Removed
/api/figma/verifystartup check: Figma's/v1/meendpoint does not accept OAuth tokens — only Personal Access Tokens. So the verify endpoint returned 401 for every valid OAuth token, and the plugin cleared the Figma connection on every reload. Removed the verify call entirely. Server-side token revocation is now detected at sync time (when a Figma API call returns 401, the sync error handler surfaces it). - Token storage read failures on iframe reload:
saveToken()only wrote to Miro board storage (server-side, slow on fresh load) and returned early, never reachinglocalStorage. On iframe reload (Miro tab reopen or idle resume),getToken()tried board storage with a 1500ms timeout; if that failed before Miro's SDK synced board data, the fallback tolocalStoragefound nothing and returnednull. Fixed by always writing tolocalStoragealongside board storage, and reading fromlocalStoragefirst (instant, no network). Board storage remains as a backup for when browser cache is cleared.
Changed
- Batch limit counts unique exports, not total widgets: The batch limit of 3 now applies to unique
(fileKey, nodeId, format, scale, platform)groups — i.e., distinct Figma/Penpot exports. Widget copies of the same frame share the render cache and are NOT counted against the limit. WhensyncAllCopiesis enabled, all copies of the first 3 frames sync without consuming extra export slots. The sync function no longer silently truncates; if called with >3 unique groups it throws an error (defense-in-depth, since the UI already blocks the button).
Changed
- Community plan scale limit (1x, 2x only): The scale selector now limits options to 1x and 2x for the Community plan (self-host deployments keep 1x–4x). This caps the worst-case export count at 3 frames × 2 scales = 6 renders per sync, protecting free-tier infrastructure (Ably 200k msg/mo, Upstash 10k cmd/day) from accidental overuse.
- Rate limit defaults bumped:
RATE_LIMIT_COMMUNITY_FIGMA_PER_MIN5→12,RATE_LIMIT_COMMUNITY_UPDATE_IMAGE_PER_MIN10→30. Community users can now run 2 full batches per minute (6 renders + copy updates). - Propagate now unchecks Preserve Size: When "Propagate format & scale to all copies" is checked, "Preserve widget size" is automatically unchecked — prevents 1x image in a 4x box (pixelated).
Added
- Version injection via generated file: Changed
src/lib/version.tsfromrequire('../../package.json')(subject to bundler caching) to importing fromsrc/lib/version.generated.ts— a file written byscripts/inject-version.mjswith hardcoded strings. The inject script now runs beforeyarn devas well asyarn build, ensuring the displayed version always matchespackage.jsonregardless of Turbopack/Webpack caching.
[0.13.0] - 2026-07-24
Added
- Batch limit of 3: Sync now limits to 3 items per operation. The UI shows a warning banner and disables the Sync button when more than 3 items are selected, preventing silent truncation mid-process.
penpot.openPage()preload for cross-page exports: When exporting a Penpot shape from a different page, the companion plugin navigates to that page before export viaawait penpot.openPage(page). This preloads the shape data into WASM memory, reducing the export freeze from 10-60s to ~1-3s (navigation flicker instead of main-thread freeze). The companion does not navigate back.- Sequential Penpot batch processing: Changed from
Promise.all(concurrent) tofor...of(sequential) to allow each export to benefit from the previousopenPagenavigation when shapes share the same page. - UI card stack height: Increased from 300px to 360px so 3 cards fit without scrolling.
Changed
- Penpot
findShapeById— Official API: Replaced the manual recursive tree walk with Penpot's officialpage.getShapeById(shapeId)API (O(1) internal map lookup by UUID) for both current-page and cross-page shape search. The tree walk is kept as a fallback for older Penpot instances. The new path is faster and more robust.
Documentation
- Penpot Export Freeze Root Cause: Documented the actual freeze mechanism — Penpot's WASM
_render_shape_pixelsloads other page's shape tree into linear memory synchronously.penpot.currentPageis read-only from the plugin API, preventing page preloading. Both WASM (PNG) and server (SVG) paths freeze for off-page shapes. penpot.openPage()workaround: Documented theopenPagenavigation preload as a mitigation for the freeze (fast page switch instead of frozen UI).- Batch limit rationale: Batch limited to 3 items due to Miro API rate limits, relay round-trip latency, and WASM page-load overhead per unique page.
[0.12.0] - 2026-07-24
Fixed
- "Place on Canvas" & "Replace Selected" Status Feedback: Both features now properly use the color-coded status bar. Success messages have
✓prefix (green success), and progress messages are shown during the render/export phase before the image is placed or replaced. Figma and Penpot import flows both report "Rendering Figma frame..." / "Exporting Penpot frame..." while the server generates the image. - Penpot Cross-Page Shape Search:
findShapeByIdinpublic/penpot-companion-plugin.jsnow falls back to searching all pages (penpot.pages) when a shape is not found on the current page. Previously, syncing a Penpot image only worked if the original frame was on the page currently open in Penpot — shapes on other pages caused a hard sync failure. Also fixed a latent bug wherefindShapeByIdwas called withoutawaitin the export-shape handler. - Penpot Export Timeout: Increased relay timeout for
export_shapefrom 18s to 120s incompanionRelayClient.ts. Some complex Penpot shapes take up to 66s to export — the previous 30s window was still too tight. - Companion Plugin Logging: Added
[SyncingBoard]console logs tofindShapeByIdandexportShapeBufferto distinguish which search path succeeds (selection, current page, cross-page, orpenpot.exportfallback) and howpenpot.pagesbehaves. - Documentation: Added Penpot export performance characteristics to
doc/architecture.md(selection fastest → same page → other page slowest). Removed session-level render cache to prevent stale image data on re-sync.
Documentation
Added
- Proactive Token Keep-Alive (Miro Plugin): New 25-minute background interval in
useAuthTokens.tssilently refreshes both Figma and Miro tokens before they reach the 5-minute expiry buffer. Prevents the "token expired mid-session" cascade that forced users to reconnect. - Figma Token Validation on Startup: After loading tokens, the plugin now calls
GET /api/figma/verify(lightweight/v1/mecheck) to detect server-side revocation. If the token is invalid, the UI state clears immediately (gray icon) instead of staying green until the first sync failure. - New
/api/figma/verifyEndpoint: Proxies a call to Figma's/v1/meendpoint with 5s timeout. Returns{ valid: true }on success, 401 on invalid/expired token.
Changed
- Token Refresh Timeout:
REFRESH_TIMEOUT_MSinsrc/lib/tokens.tsincreased from 7s to 15s, andPROVIDER_TIMEOUT_MSin the refresh API route increased from 8s to 15s. Provides sufficient runway for Vercel cold starts (~3-5s) plus OAuth provider latency without timing out. - Headless SDK Wait: Miro SDK detection timeout in
useMiroSelection.tsincreased from 8s to 20s for headless (app icon) mode, with up to 3 retries at 5s intervals. Panel mode uses the original 8s timeout with a single attempt. Mirrors the same retry pattern already proven inuseAuthTokens.
Fixed
- Connection Stability Cascade: The combination of longer timeouts, proactive keep-alive, and startup validation addresses the interconnected failure chain documented in v0.11.0 investigation:
- Token refresh no longer races against cold-start serverless execution (15s > 10s Vercel max on cold boot).
- Background keep-alive keeps Vercel instances warm for sync-initiated refreshes.
- Startup validation catches server-side revoked tokens without waiting for a user action.
[0.11.0] - 2026-07-24
Added
- Ably Channel Separation (Figma vs Penpot): Figma and Penpot companion plugins now use separate Ably channels (
figma:{pairingId}/penpot:{pairingId}) instead of both subscribing topenpot:{pairingId}. Eliminates cross-talk where Figma responses would appear in the Penpot Import tab (showing "unknown-file" with Figma frame names) and vice versa.src/lib/relayAbly.ts:publishPenpotCommand,isPenpotOnlineAbly,generateAblyTokenacceptplatformparameter.public/figma-companion-ui.html: Changed Ably channel frompenpot:tofigma:prefix.src/app/miro-plugin/companionRelayClient.ts:getAblyConnectionandcallRelaypass platform to token/channel. Cache key now includescurrentConnectedPlatformto prevent stale connection reuse when switching tabs.src/app/api/relay/request/route.ts: Acceptsplatformin request body.src/app/api/ably/token/route.ts: Token generation uses platform-specific capability.
Changed
- Header Layout (Miro Plugin): Logo now aligns with first text line (
items-start+mt-0.5), version/tier moved to a centered footer above the status bar. - Miro Connection Icon: Enlarged from
w-4 h-4(16px) tow-[18px] h-[18px]for better visibility. - Version Bump: 0.8.0 → 0.11.0 across all plugins.
figma-companion-ui.htmladded to the injection script. - Color-Coded Status Bar: Replaced the single string
syncStatuswithSyncStatus { message, type }where type is'success' | 'error' | 'progress' | 'info'. Footer renders with appropriate colors (green/red/amber/gray) and a pulsing dot during progress states. Type is inferred from message content automatically for backward compatibility.
Fixed
- "Selected Frame" Name Overwrite in Sync: Three-layer fix preventing the Penpot companion plugin's default name
'Selected Frame'from overwriting real widget names:public/penpot-companion-plugin.js: Changed defaultshapeNamefrom'Selected Frame'tonullwhenfindShapeByIdreturns null.src/app/miro-plugin/useMiroSync.ts:nameCachenow rejectscontent.name === 'Selected Frame'.src/app/miro-plugin/usePenpotImporter.ts: BothsetPenpotNodeInfoandresolvedNamereject the placeholder.
- Ably Connection Cache Miss:
getAblyConnectionnow includescurrentConnectedPlatformin the cache key, preventing stale connections when switching between Figma and Penpot with the same pairing ID.
[0.10.0] - 2026-07-22
Added
- "Replace Selected" — Adopt Any Image into SyncingBoard: New button in Import tab that replaces a manually-pasted or third-party image widget with a SyncingBoard-managed copy, keeping the widget ID intact to preserve connectors, comments, links, and frame membership.
- "Replace selected" button below each Import button (Figma/Penpot), enabled when a frame is selected.
- Reads the current Miro board selection and adopts any image-type widgets.
- Attaches
syncingboardmetadata (adoption) or updates it (re-targeting to a different frame). - Then renders and pushes the new image via the standard sync API.
- Non-SyncingBoard images become recognised copies; existing SyncingBoard widgets can be re-targeted to a different frame.
- SEO & Analytics Overhaul: Made the public site discoverable and measurable.
- Added
robots.ts(disallow/api/and/miro-plugin) and dynamicsitemap.tscovering all docs pages. - Added Open Graph tags (
og:title,og:description,og:image,og:url,og:type) and Twitter Cards (summary_large_image). - Added JSON-LD structured data (
WebSiteschema), canonical URL, andmeta keywords. - Added
generateMetadata()per doc page for unique titles/descriptions; fixed breadcrumb<span>→<h1>. - Added Google Analytics (G-Q4W94QDWWC) with gtag tracking for page views.
- Added custom event tracking:
sync_start,sync_complete,sync_error,oauth_attempt,oauth_connect,oauth_disconnect. - Added CookieConsent banner with accept/decline (hidden inside Miro plugin iframe).
- Upgraded to Google Consent Mode v2: default
analytics_storage: 'denied'before GA loads; grants on accept. - Added
GET /api/healthendpoint returning{ status, name, version, timestamp, uptime }.
- Added
Fixed
- Code block contrast (WCAG AA): Light-mode syntax highlighting colors now pass 4.5:1 minimum contrast ratio against the cream code background (
#e8e4dc). Keywords dark teal#005a73, strings dark green#0d6e30, functions dark violet#5b1fa8. Dark mode unchanged.
[0.9.0] - 2026-07-20
Added
- "Preserve Widget Size" Option on Sync: Added a checkbox in the Sync panel that allows users to refresh image content only — without resizing the widget — preserving any manual crop, scale, or layout adjustments made in Miro.
- New "Preserve widget size" checkbox in the sync panel, positioned between "Also update all board copies" and "Propagate format & scale".
- When checked, the API skips the geometry PATCH (Step 2) and only uploads the image.
- Handles aspect ratio shifts by setting Miro's
style.fit: "contain"property rather than stretching the image. - Defaults to unchecked (current resize-on-sync behavior) to avoid surprise.
- Works independently of "Propagate format & scale".
[0.8.0] - 2026-07-19
Added
- Event-Driven WebSocket Relay Architecture: Refactored selection detection and image sync pipelines to eliminate server-side polling loops, reducing Upstash Redis command usage by 90% and Vercel serverless execution time by 95%.
- Direct Selection Transport (0 Redis Commands): Figma and Penpot companion plugins publish selection details (
id,name,fileKey) directly over Ably WebSockets to the Miro plugin sidebar, bypassing Redis entirely. - Hybrid Image Export (3 Redis Commands): Heavy base64 image exports are uploaded to Vercel/Redis, followed by publishing a tiny
'result-ready'event notification over Ably. Miro receives the WebSocket event and reads/deletes the image payload in a singleGET /api/relay/responsecall.
- Direct Selection Transport (0 Redis Commands): Figma and Penpot companion plugins publish selection details (
- Client-Side Ably Bridge in Miro: Integrated direct Ably WebSocket client connections inside the Miro plugin sidebar to listen for companion response events in real-time.
- Unified Companion Relay Client: Renamed
penpotMcpClient.tstocompanionRelayClient.tsto reflect its unified role as the Cloud Relay client for both Figma and Penpot companions.
Fixed
- Ably Publish Capability Permission: Updated
generateAblyTokeninsrc/lib/relayAbly.tsto grant['publish', 'subscribe', 'presence']capabilities on pairing channels, resolving Ably40160: Unable to publish message due to lacking publish capabilityerrors. - Subscription Race Condition: Restructured
callRelayinsidecompanionRelayClient.tsto subscribe to Ably events and set up early-results buffering before sending HTTP trigger requests to Vercel, completely resolving 10-second timeout errors. - Penpot Export Shape Lookup: Updated
findShapeByIdinpublic/penpot-companion-plugin.jsto prioritize active selection (penpot.selection[0]) and nativefindShapeAPI methods, resolvingPenpot export API unavailable in this runtimeandunknown-fileID fallbacks. - Direct Cloud Relay Routing: Removed legacy
http://127.0.0.1:3845/mcpfetch fallbacks inuseFigmaImporter.ts, eliminating browser Private Network Access (PNA) CORS warnings and 2-second connection delays on HTTPS.
Security
- Header-based Token Transmission: Refactored
/api/miro/update-imageand/api/oauth/refreshto receive sensitive tokens via HTTP headers (Authorization: Bearer,X-Figma-Token,X-Refresh-Token) instead of POST body, preventing credential leakage in proxy/WAF logs. - Tauri Webview CSP Hardening: Replaced disabled CSP (
null) with a strict policy restricting scripts, styles, images, and connections to'self'only, mitigating XSS and code injection in the local bridge webview.
[0.7.1] - 2026-07-18
Added
- Document-Level Figma Pairing: Implemented a document-level linking system to support syncing from multiple different Figma files to a single Miro board without credential collisions.
- Refactored
figma-plugin/code.jsto save and read pairing keys using document metadata storage APIs (figma.root.setPluginData/figma.root.getPluginData). - Added an inline "Pair Figma Design File" input box inside the hosted companion panel (
public/figma-companion-ui.html) that prompts the user exactly once per file and links the document permanently. - Dynamically propagates the saved file key via query parameters when loading the companion iframe.
- Refactored
- Limitation Documentation: Documented the Figma public API security limitations (blocking automated
figma.fileKeyreads in Community plugins) and how self-hosters can enable it automatically using theenablePrivatePluginApimanifest flag indoc/architecture.mdanddoc/faq.md.
Fixed
- Ably Selection Bridge Sync:
- Corrected Ably event subscription from
'select'to'command'inpublic/figma-companion-ui.htmlto align with the backend router protocol. - Appended the
pairingIdquery parameters to the/api/ably/tokentoken request inside the companion UI, resolving the HTTP 400 Bad Request error. - Prefixed the Ably channel key with
'penpot:'to align with backend security tokens.
- Corrected Ably event subscription from
[0.7.0] - 2026-07-17
Added
- Figma Companion Plugin (Cloud Relay): Built a Figma companion plugin that enables real-time selection auto-detect over the cloud relay using Ably.
- Created
figma-plugin/directory containingmanifest.json, local sandbox controllercode.js, andui.htmlmessage relay bridge. - Implemented the hosted
public/figma-companion-ui.htmlstatic asset with pairing connection status indicators, Ably subscriptions, and parent window message listeners. - Added a configuration panel in the local plugin UI so self-hosts can easily point the companion to their own deployed SyncingBoard domain URL.
- Refactored
useFigmaImporter.tsto fallback to Cloud Relay queries (Figma Companion) if the local Tauri MCP server/SyncBridge is not running.
- Created
- White-Labeling & Marketplace Setup Docs: Updated setup and architectural guides detailing the plug-and-play Community installation paths from official marketplaces, alongside a customization guide for renaming plugins, updating brand logo icons, and adjusting CSS theme variables.
[0.6.2] - 2026-07-17
Added
- Ably and Upstash Badges: Added Ably Realtime and Upstash Redis status badges to the top of
README.md.
Fixed
- Companion Status Layout Simplification: Renamed status labels to clearly distinguish between local and cloud connections, and removed the redundant third "Active Connection" status row from
public/penpot-companion-ui.html. - Markdown Card Description Fallback Heuristic: Updated
extractDescriptioninsidesrc/lib/docs.tsto skip headings, blockquotes, HTML tables, and badge links, allowing repository README card previews on the website to correctly extract the initial text introduction.
[0.6.1] - 2026-07-16
Added
- FAQ Document: Created a Frequently Asked Questions (FAQ) guide under
doc/faq.mdcovering concurrent collaboration rules, metadata signatures, Chrome PNA network blocks, security configurations, and image format options.
Fixed
- Penpot Companion Window Height: Increased the companion iframe height from
480to600to prevent unnecessary vertical scrollbars in the Penpot editor interface. - Markdown Description Parsing: Updated description extraction logic to read
description:from YAML frontmatter first, preventing the FAQ page card from displaying the first question's answer as its description. - CRLF Line Endings Fix: Refactored
getDocBySlugto strip all carriage returns (\r) from the document content before MDX compilation. This resolves issues where trailing carriage returns (\r) in Windows line endings broke the MDX markdown parser, causing links/badges to show as raw text and the License document to render raw ````text`. - Inline Badges Rendering: Added a CSS override for images in prose paragraphs to render markdown badges inline-block rather than stacking them vertically. Removed the raw
<table>wrapper fromREADME.mdthat was failing to parse in MDX.
[0.6.0] - 2026-07-15
Added
- Community Plan Rate Limiting: Token-based rate limiting that identifies users by their OAuth token hash (or Penpot pairingId) instead of IP, making it immune to VPN cycling. Edge middleware, per-endpoint
withRateLimit()HOF, and global daily backstop (500 syncs/day all users). - Dual-backend rate limiter: Auto-detects Redis (
@upstash/ratelimit) ifUPSTASH_REDIS_REST_URLis set, otherwise uses in-memory sliding window (persistent infra only). Falls back gracefully on Vercel without Redis. - Configurable via env vars: 11
RATE_LIMIT_COMMUNITY_*variables for all per-endpoint and global limits, plusRATE_LIMIT_ENABLED=falseto disable entirely. - Setup guide: Rate limiting section in
doc/setup.mdwith env var table and multi-layer explanation. - README callout: Public demo notice with link to rate limiting docs.
[0.5.7] - 2026-07-14
Added
- Secure Key Generation: Migrated pairing ID and OAuth state generation to cryptographically secure random generators using
window.crypto.getRandomValues. - Redis OAuth Token Cache: Replaced the vulnerable global in-memory OAuth state cache with Upstash Redis storage featuring a 300-second TTL and automatic deletion on consumption.
- CORS Origin Whitelisting: Configured Tauri's local Axum bridge server to validate CORS
Originheaders against a whitelist of trusted domains (https://syncingboard.com,http://localhost:3000,http://localhost:1420). - Dynamic OAuth Host Detection: Configured OAuth endpoints to dynamically parse request headers (
hostandx-forwarded-proto) to compute redirect URIs, resolving state/cookie CSRF errors on Vercel preview environments and custom subdomains. - Miro Direct Install Bypass: Allowed empty state parameter validation in the Miro callback if no local CSRF cookie exists, enabling developers to install the app directly from the Miro Developer Dashboard (which does not provide a state parameter).
Changed
- Read-Only Pairing IDs: Restricted the pairing ID input field in the Miro companion sidebar to be read-only (
readOnly={true}) so users can only copy their generated keys, preventing weak/custom key injection. - Unified Penpot Cloud Transport: Removed local Tauri bridge routes for Penpot communication, unifying all Penpot select and export commands over the secure cloud relay pathway (Ably + Redis).
Removed
- Orphan API Routes: Cleaned up unused endpoints
GET /api/relay/penpot/pollandPOST /api/relay/penpot/register. - Orphan Tauri Bridge Route handlers: Pruned legacy local WS (
/ws), local polling (/penpot/poll), register (/penpot/register), result (/penpot/result), and local command triggers (/detect-penpot,/export-penpot) from the Tauri desktop app's Axum server. - Obsolete Temp Files: Deleted scratch files
._temp_comp.htmland_temp_section.txt.
Fixed
- API Error Leakage Sanitization: Sanitized output exceptions in OAuth refresh and Miro image update endpoints to return generic error messages instead of raw system stack traces.
[0.5.6] - 2026-07-14
Added
- Penpot Natural Dimensions: Companion export and selection responses now include shape width/height from selrect. Stored in widget metadata during import and used as canonical display size for sync resize calculations.
- Widget Metadata Update After Sync: After each PATCH succeeds, widget metadata (format, scale, width, height) is refreshed via the Miro Web SDK so the format/scale dropdown shows current values on next selection.
getById(id)added to MiroBoard type definition.
Fixed
- Miro Token Stale-Expiry on Sync:
syncSelectedScreensnow callsgetValidToken('miro')at the start to auto-refresh the token before syncing, instead of relying on the mount-time token. - Penpot Import Width Hardcode: Removed
width: 800fromcreateImage()inusePenpotImporter.ts(same fix previously applied to Figma). - Missing Width in Selection State:
SyncedImagenow includes width from the Miro widget. The sync selected-items path passes width to the PATCH endpoint, enabling resize. - No Scale Passed to Penpot Export in Sync: The
export_shapecall was missing the scale parameter --- always defaulted to 2 during propagate. Now passestarget.scaleso the selected scale takes effect. - Render Cache Key Collisions: Cache keys now include scale (
fileKey|nodeId|format|scale) for both Figma and Penpot, preventing collisions when copies have different scales. - Companion Plugin Status Stuck on Unknown: Handshake waited for a
ui-readymessage that is never received by the UI. Now sets plugin status to Connected ontheme-change(the plugin's actual handshake response). - SVG Widget 0-Width Resize Fail: Miro SDK returns
width: 0for SVG image widgets. Width calculation now handles 0-width gracefully --- uses stored natural width when available, otherwise skips geometry (lets Miro auto-size). - Miro PATCH Geometry Override by Async Image Processing: Miro's image-specific PATCH overrides
geometry.widthwith the new image's pixel dimensions after async processing. Fixed by splitting into two steps: (1) upload image via image endpoint (no geometry), (2) apply geometry via generic item endpoint (JSON body) which updates the widget data model directly without triggering image reprocessing.
Changed
- Penpot Import Display Width: Display width now calculated as
naturalWidth x exportScale(not fixed at natural width). Widgets visually scale with export resolution: 1x=400px, 2x=800px, 4x=1600px. - Sync Resize Uses Natural Width: For Penpot items with stored natural width, display width =
naturalWidth x effectiveScale. Propagate changes now resize the widget proportionally. - Export Filename in Miro PATCH: Image filename sent to Miro uses the actual
nodeNameinstead of hardcodedscreenshot.png. Sanitizes invalid filename characters.
[0.5.5] - 2026-07-11
Added
- Ably WebSocket Transport for Penpot Commands: Replaced Redis polling for command delivery with Ably pub/sub. Companion now subscribes to an Ably channel via WebSocket for near-instant command delivery with zero idle Redis cost.
- Added
src/lib/relayAbly.ts--- Ably REST helpers for publishing commands and token generation. - Added
POST /api/ably/tokenendpoint --- generates scoped subscribe-only tokens for companion authentication. - Updated
POST /api/relay/request--- publishes commands via Ably instead of Redis LPUSH. - Updated
public/penpot-companion-ui.html--- replaced polling loop with Ably Realtime WebSocket subscription.
- Added
- Presence via Ably: Companion enters Ably channel presence on connect;
/api/relay/requestchecks Ably presence REST API (instead of Redis SETEX) to determine if companion is online.
Removed
- Redis-based
enqueuePenpotCommand,dequeuePenpotCommand,isPenpotOnlinefunctions (command delivery fully migrated to Ably). - Period heartbeat to
/api/relay/penpot/register(no longer needed --- Ably presence replaces it).
Notes
- Result storage remains on Redis (
storeRelayResponse/getRelayResponse/deleteRelayResponse) --- these are only used during active imports, with negligible idle cost. - Fallback endpoints preserved:
/api/relay/penpot/poll(BRPOP) and/api/relay/penpot/registerremain operational for non-Ably clients. - Requires
ABLY_API_KEYenvironment variable. Free tier (200k messages/month) is sufficient.
[0.5.4] - 2026-07-11
Changed
- Penpot Relay Transport: Switched companion command retrieval from short polling to long-polling.
/api/relay/penpot/pollnow blocks up to 45s waiting for queued commands (BRPOP), then responds immediately when work arrives. - Companion Poll Loop: Updated
public/penpot-companion-ui.htmlto use persistent long-poll cycles (no 2s idle spin loop), reducing relay command churn while keeping near-real-time command pickup. - Presence Heartbeat Strategy: Removed per-poll presence writes. Companion now sends explicit heartbeat registration at connect and every 60s, preventing extra Redis writes on every empty poll cycle.
[0.5.3] - 2026-07-11
Fixed
- Miro Connection Reliability (Yellow Forever): Refactored token bootstrap in
useAuthTokens.tsto prevent perpetual loading states with bounded retries and deterministic loading-settle behavior. - Miro SDK Storage Proxy Errors: Hardened
src/lib/tokens.tswith strict runtime callability checks forboard.storage.get/setplus short operation timeouts and localStorage fallback. - OAuth Refresh Stall Protection: Added timeout handling in both client refresh calls (
tokens.ts) and provider refresh route (/api/oauth/refresh) to prevent hanging refresh chains. - React Hydration Error #418: Removed SSR/client mismatch sources in Miro plugin initialization by moving client-only reads (
window.location,localStorage, random pairing id generation) to mount-time effects. - Miro OAuth Connected-but-Gray Regression: Normalized OAuth token payload handling in
useAuthTokens.tsso Miro callbacks/polling accept valid access tokens even whenrefreshTokenis missing. - Miro Callback Token Shape Robustness: Updated
src/app/api/oauth/miro/callback/route.tsto always serializeaccessToken/refreshToken/teamIdas strings for stable popup handoff payloads. - Token Reload Tolerance: Updated
src/lib/tokens.tsparsing to tolerate tokens withoutrefreshToken(fallback'') and keep using valid access tokens until actual expiry when refresh tokens are absent. - Penpot Companion Theme Mismatch: Added explicit UI-ready handshake (
ui-ready) betweenpublic/penpot-companion-ui.htmlandpublic/penpot-companion-plugin.js, with theme normalization and startup fallback.
[0.5.2] - 2026-07-11
Added
- Import Format & Scale Selectors: Added format (SVG/PNG) and scale (1x-4x, visible when PNG selected) dropdowns to both Figma and Penpot node info cards in the import tab, matching the sync grouped-card UI.
importFigmaScreenandimportPenpotScreennow acceptformatandscaleparameters.
Fixed
- Figma MCP Tool Name (SyncBridge & Browser Fallback): Updated
get_design_context->get_selectionin bothtauri-bridge/src-tauri/src/lib.rsandsrc/app/miro-plugin/useFigmaImporter.tsto match the current Figma Desktop MCP API. - Penpot Companion Polling Flood: Added 2-second delay (
await sleep(2000)) between poll iterations inpublic/penpot-companion-ui.htmlto prevent ~1,000 Redis commands/second when idle. Tight loop was the cause of unexpectedly high Redis consumption (~2,600 commands for 10-20 syncs).
[0.5.1] - 2026-07-11
Added
- Penpot Relay API (Upstash-backed): Added
/api/relay/request,/api/relay/penpot/register,/api/relay/penpot/poll, and/api/relay/penpot/resultto relay Penpot selection/export commands over public HTTPS instead of localhost transport. - Relay Store Module: Added
src/lib/relayRedis.tswith strict typed command queue helpers, presence heartbeat keys, response TTL caching, and key sanitization.
Changed
- Penpot Transport Default:
src/app/miro-plugin/penpotMcpClient.tsnow defaults to cloud relay mode and keeps SyncBridge/Tauri as an optional fallback. - Companion UI Endpoint Routing:
public/penpot-companion-ui.htmlnow talks to/api/relay/penpot/*endpoints and no longer depends ontargetAddressSpacelocalhost access. - Settings UX: Pairing ID is now always visible in the Miro plugin settings so Penpot can pair in both relay and SyncBridge modes.
Fixed
- PNA/LNA Block in Penpot Web Context: Removed hard dependency on browser-to-localhost calls for Penpot sync path, preventing
ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKSin relay mode.
Documentation
- Architecture Reassessment: Updated
doc/architecture.mdwith phase 7 (cloud-relay-first + Tauri as capability extender), revised sections 1.B, 2, and 5.B to reflect relay-first reality. - Backlog Restructure:
doc/backlog.mdreorganized with new "Tauri Capability Extender" section (large images, Adobe UXP, local LLMs, compression, document parsing, two-way sync, multi-whiteboard) and an Icebox for archived bridge architectures.
[0.5.0] - 2026-07-11
Fixed
- Penpot PNA Bypass: Replaced WebSocket connection in Penpot companion plugin with HTTP
fetch()polling to bypass Chrome's Private Network Access restrictions. Thefetch()API supportstargetAddressSpace: 'loopback'which allows public web pages (Penpot) to connect to local loopback servers (SyncBridge) after user approval. - Bridge HTTP Polling: Added three new SyncBridge endpoints for HTTP-based command queuing:
POST /penpot/register,GET /penpot/poll, andPOST /penpot/result. The companion plugin now polls for commands every ~1 second instead of maintaining a WebSocket connection. - Command Queue Architecture: Modified
handle_detect_penpotandhandle_export_penpotto enqueue commands in a per-pairingId queue instead of sending via WebSocket. The polling handler (handle_penpot_poll) waits up to 30 seconds for commands using long-poll with tokioNotifysignaling.
[0.4.0] - 2026-07-11
Added
- Documentation Site: Replaced
/dashboardwith a full documentation site at/docs. Rendersdoc/*.mdas styled pages with TOC sidebar, syntax highlighting, heading anchor links, and a metadata bar (last updated, word count). - Agent-Friendly Docs API: Added
GET /api/docs/list(JSON index) andGET /api/docs/raw?file=<filename>(raw markdown) for AI agent consumption.backlog.mdis hidden from public. - Token Fingerprinting: Token storage keys now include a
deploymentFingerprint()hash ofwindow.location.originto prevent collisions across SyncingBoard deployments. - 19 API Route Tests: Test suites for
/api/figma/render,/api/figma/render-batch,/api/figma/node-info, and/api/miro/update-image(38 total, all passing).
Changed
- Token Refresh Resilience:
getValidToken()no longer clears the token on a single refresh failure. The old token stays in storage and retries on the next page load, preventing unnecessary re-authentication from transient failures. - Enhanced Bridge Logging: All SyncBridge events now show
[Service]prefixes ([Bridge],[Figma],[Penpot]) with pairing IDs, shape names, file keys, and session counts.
Fixed
- Penpot WebSocket PNA: Added explicit
OPTIONShandler for the/wsroute in the bridge. Chrome's Private Network Access preflight is now properly answered withAccess-Control-Allow-Private-Network: true, fixingERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS.
[0.3.0] - 2026-07-11
Added
- Penpot Manifest File: Created
public/penpot-manifest.jsonspecifying metadata, permissions, entry point, and icon links to enable native custom plugin registration inside the Penpot Workspace editor.
[0.2.3] - 2026-07-11
Changed
- Unified Badge Accent Colors: Updated both Figma and Penpot transparent outline selection badges in the Sync tab to use the exact same cyan brand accent color (
text-accentandborder-accent/40) for UI design consistency.
[0.2.2] - 2026-07-11
Changed
- Clean Platform Badges: Replaced dark background colored badges in the Sync tab selection cards with transparent background, outline-only badges styled in each platform's accent color (neon green for Figma, purple for Penpot).
Removed
- Redundant Penpot Card: Completely removed the redundant disabled "Penpot Syncing - INACTIVE" card from the Settings panel when SyncBridge is disconnected. All local workspace pairing states are now cleanly represented by the SyncBridge card itself.
[0.2.1] - 2026-07-11
Changed
- SyncBridge Rebranding: Renamed all occurrences of "Tauri Desktop Bridge" to "SyncBridge" across the codebase, user interface, error messages, and documentation.
- SyncBridge UI Alignment: Aligned the SyncBridge connection card in the Settings panel to match the Connect/Disconnect pattern of Figma and Miro (replacing the toggle checkbox).
Fixed
- Tokio Runtime Panic: Switched background server thread initialization from
tokio::spawntotauri::async_runtime::spawnto resolve the "there is no reactor running" startup panic in the compiled binary. - Penpot Selection Pointers: Cleaned up outdated selection error tip reminders inside
usePenpotImporter.ts.
[0.2.0] - 2026-07-11
Added
- Tauri Desktop App Workspace: Initialized standard Tauri v2 application inside
/tauri-bridgeusing Yarn and Vanilla TypeScript template. - Axum Local Secure Server: Implemented Axum HTTPS local web server on port
4401using rustls-tls to route selection detection and exports. - WebSocket Pairing Router: Setup a thread-safe connection mapping WebSocket upgrade path in Axum to pair and relay commands to/from active Penpot browser tabs.
- Figma desktop relay: Structured automatic local HTTP selection query forwarding to Figma desktop's MCP instance on port
3845. - Automated CI/CD Release Pipeline: Created
.github/workflows/release-tauri.ymlwhich automatically compiles.msi,.exe,.dmg,.app, and.debinstallers using GitHub actions upon tagging releases. - Bridge Documentation: Added
doc/tauri-setup.mddetailing prerequisites, Let's Encrypt certificates installation, and GitHub Action releases.
[0.1.11] - 2026-07-11
Added
- Tauri HTTPS Bridge & Penpot Companion Plugin Schema: Created the system specifications and architecture design for the loopback bridge.
- Penpot Companion Plugin: Created
penpot-companion-plugin.htmlscript which connects the Penpot editor tab directly to the Tauri proxy over WebSockets. - Tauri client support: Configured
penpotMcpClient.tsto connect to Tauri secure loopbacklocal-syncingboard.luiskobayashi.comwhen the bridge toggle is active. - Figma Tauri support: Enabled local Figma selection detection through the Tauri proxy inside
useFigmaImporter.ts. - Sidebar settings toggle: Added a Connect/Disconnect widget in the settings tab for "SyncBridge" along with a pairing ID generator and clipboard copy utility.
Removed
- Penpot MCP Server Client: Deleted all redundant Penpot MCP server connection code from
penpotMcpClient.ts, transitioning exclusively to the Tauri secure loopback bridge.
[0.1.10] - 2026-07-10
Added
- Penpot Sync Integration: Added support for syncing Penpot frames to the Miro canvas side-by-side with Figma.
- Penpot MCP Client Integration: Created
penpotMcpClient.tscommunicating with the local Penpot MCP server over HTTP JSON-RPC POST requests to prevent SSE timeout locks. - Penpot Client Importer: Built
usePenpotImporter.tsvalidating frame URLs, detecting selection frames, and placing SVGs on the canvas. - Miro Update API Platform Handling: Updated
/api/miro/update-imageto support and output platform-specific title tags (PenpotSyncvs.SyncingBoard). - Consolidated Selection UI: Grouped duplicate canvas screens in the sidebar under a single frame card, rendering a copy counter badge (e.g.
x3) in the top-right and batch-applying format/scale changes to all selected copies. - CORS Support for Penpot MCP: Patched the local Penpot MCP server code (
PenpotMcpServer.ts) to support cross-origin requests, enabling browser-based plugin communication.
[0.1.9] - 2026-07-10
Added
- SVG Vector Support: Integrated vector format rendering, enabling users to sync screens as SVGs on the Miro board for infinite zoom crispness.
- Per-Image Formatting & Scaling: Added interactive Format (PNG/SVG) and Scale (1x, 2x, 3x, 4x) controls in the Sync sidebar panel for each selected image widget, dynamically stored inside Miro's metadata.
- Preferences Panel: Added a global "Default PNG Scale" configuration dropdown inside the settings tab to set the default scale for newly imported images.
- Mixed Batch Grouping: Upgraded the rendering engine to group requests by fileKey + format + scale, keeping mixed sync selections batched and optimized.
[0.1.8] - 2026-07-10
Added
- Vitest Unit Test Suite: Configured Vitest and jsdom environments for frontend testing. Added test coverage for Figma URL parsing and OAuth token validation helpers.
- Husky Pre-Push Hook: Added automated pre-push hook integration ensuring lint, test, and production builds pass before any git push.
- Themed Auth Popups: Integrated a dynamic, client-side script in all OAuth auth and callback popup windows to detect the active theme configuration (
light,dark, orsystempref) from localStorage and dynamically style background, text, buttons, and loading states to match. - Name-First Title Format: Changed the image title structure to show the clean human-readable design name first, followed by the sync metadata (
Name [SyncingBoard|fileKey|nodeId]). Adapted selection hook parsing, fallback generation, and copy-matching logic accordingly.
Fixed
- OAuth CSRF Security: Implemented cryptographic random
statevalidation via secure, HTTP-only cookie validation for Figma and Miro callback routes. - Safe Token Serialization: Transitioned from unsafe string template literals to robust
JSON.stringifyserialization on authorization success callback frames to prevent script crash and potential injection. - Verbose Console Logs Cleanups: Removed development debugging logs from
useMiroSelection.tsto follow production standards.
[0.1.7] - 2026-07-09
Fixed
- Iframe Token Write Missing: Fixed the core token persistence bug by updating the
postMessageandBroadcastChannelevent handlers insideuseAuthTokens.tsto explicitly callsaveToken()when receiving successful authentication results from the OAuth popups. This ensures credentials are saved to Miro's board storage right away instead of only existing in temporary component memory.
[0.1.6] - 2026-07-09
Added
- SyncingBoard Custom Logo: Integrated
public/syncingboard_logo.svgas the application's favicon and main sidebar logo, styled with dynamic CSS mask-image logic. - Offline Font Optimization: Replaced external Google Font loads with standard system font fallback stacks, preventing Next.js Turbopack compilation crashes in offline or restricted-network environments.
[0.1.5] - 2026-07-09
Added
- Dynamic Public SVG Masks: Migrated connection status indicators to use
/Figma.svgand/Miro.svgassets from the public directory. Applied CSSmask-imageinpage.tsxto colorize them into monochrome theme states (muted gray when disconnected, neon green/purple accent when connected).
Fixed
- Miro Storage Typings Parity: Reverted
lib/tokens.tsto use directstorage.getandstorage.setAPI parameters, resolving TypeScript compilation errors while keeping the initialization poll delay intact to guarantee token persistence on reload.
[0.1.4] - 2026-07-09
Added
- Connection Status Indicators: Added Figma and Miro status icons to the top-right corner of the App Header. The SVGs remain light gray (
text-text-muted/20) when disconnected and light up in active green/purple (text-accent) when authorized.
Fixed
- Token Persistence on Refresh: Fixed a race condition where tokens failed to load on page reload.
useAuthTokensnow polls and waits forwindow.miro.boardinitialization before querying board storage, preventing default browser third-partylocalStorageblocks inside the Miro iframe environment.
[0.1.3] - 2026-07-09
Fixed
- Theme Hydration Cascading Renders: Wrapped theme loading state setter inside
requestAnimationFrameto defer updates to the next microtask, preventing Next.js hydration warning loops. - Access Before Declaration: Moved local declaration blocks in
ThemeToggle.tsxabove usage patterns. - Unused Variable Warnings: Removed unused imports (
useEffect,TokenData), unused error parameters in try-catch statements, and unused Request signatures in Next.js OAuth API route handlers.
[0.1.2] - 2026-07-09
Added
- Vercel Serverless Configurations: Created
vercel.jsonto extend the serverless function execution timeoutmaxDurationto 60 seconds (applicable for Pro/Enterprise) to support heavy asset downloads.
Changed
- Vercel Deploy Destination: Updated the target destination repository URL in the "Deploy with Vercel" markdown button to point to the active
luismichio/syncingboardrepository. - Rate Limits Documentation: Expanded the
README.mdto detail both Figma and Miro rate limits, highlighting plan limitations (Starter vs. Pro) and the built-in Miro request throttle delay.
[0.1.1] - 2026-07-09
Added
- Render Batching API: Added
/api/figma/render-batchserverless route accepting multiple node IDs to render and download assets in a single Figma API request, minimizing quota usage. - Miro Sync Copy Option: Added a toggle checkbox "Also update all board copies" to the Sync tab.
- Enriched 429 Error Fields: Extracted Figma-specific rate limiting headers (
X-Figma-Plan-Tier,X-Figma-Rate-Limit-Type,Retry-After) and bubble them up to the UI status message.
Changed
- Default Sync Scope: Refactored
useMiroSyncto update only the selected board items by default (rather than scanning the entire board for copies). - Deduplicated Rendering: Sync now fetches Figma renders exactly once per unique node ID and distributes the data url to all Miro matching widgets, reducing redundant API hits to 0 for duplicated widgets.
- Error Handling: Client-side error messages now present structured details showing Plan Tiers, Seat Types, and dynamic cooldown counts.
[0.1.0] - 2026-07-08
Added
- Tabbed Plugin Layout: Rebuilt the sidebar UI in
src/app/miro-plugin/page.tsxinto an organized 3-tab layout (Sync Selection, Import Screen, Settings) to suit narrow plugin sidebar views. - Disconnect Actions: Added UI buttons to disconnect Miro and Figma connections and flush tokens from localStorage/board storage.
- Iframe Message Listener: Implemented a
window.addEventListener('message')listener insideuseAuthTokens.tsto bypass standard BroadcastChannel partitioning issues during OAuth popup redirection.
Fixed
- Miro SDK v2 Promise Proxy Crash: Resolved standard
SdkError: Cannot call method '.then()'runtime errors by wrapping proxy board resolutions into static objects. - Lazy State Hydration: Refactored the main coordinator state hooks in
useMiroPlugin.tsto use functional lazy initializers, resolving React cascading rendering warnings. - TypeScript Strict Types: Cleaned up code structure, replacing all standard
anytype overrides with strict type definitions.