0.3.2: sandboxed card rendering and reliability fixes

Merged
1C
3d ago
+769-64

Summary

Safer rendering. Cards render with JavaScript off, and the page can only load data: URLs, the brand font host (plus fonts.gstatic.com for Google Fonts) and hosts listed in the new card.allowedHosts (HTTPS only). Post text inserted with {{{raw}}} can't run scripts or send data anywhere while a card renders. Checked by rendering a hostile template: 0.3.0 ran the injected script, this branch doesn't. The example cards render the same as before.

Reliability

  • A card that can never render counts towards maxAttempts and stops retrying, instead of failing every run.
  • Mastodon: an unclear failure (timeout, 5xx) is retried only within 50 minutes of the first attempt, while Mastodon still remembers the idempotency key. After that it's recorded as uncertain. Done with a generic "retry before" deadline on the ledger entry, checked against the publish run's own clock.
  • Lock: a lock file another process has just created gets a moment to be written, and is taken if it's released meanwhile, instead of being reported as unreadable.
  • Webhook replies that claim to be JSON but don't parse are logged as a warning.

Lint and doctor

  • rules.requireAlt makes missing alt text on image posts an error (it stays a warning by default).
  • postline doctor checks brand colours against WCAG AA contrast (text, muted and chip text at 4.5:1, accent at 3:1). Both example brands pass.

Docs, changelog and version 0.3.2.

Test plan

  • npm test: 110 pass (new: render allow-list, contrast, requireAlt, render-failure cap, Mastodon window inside and after, lock released while waiting, webhook bad JSON)
  • Both examples re-rendered with the sandbox; fonts and the custom quote layout look identical
  • Hostile template: script blocked on this branch, ran on 0.3.0
  • Site builds at 0.3.2

Summary by cubic

Sandboxes card rendering so injected post text can't run scripts or send data, and fixes several reliability and publishing gaps.

  • Cards now render with JavaScript disabled and load only from the font host, data: URLs, and hosts in card.allowedHosts (HTTPS only); a non-HTTPS brand.font.url is now a config error.
  • A card that can never render now counts towards maxAttempts and stops retrying instead of failing every publish run.
  • Mastodon retries an unclear failure only within 50 minutes of the first attempt, while the idempotency key is remembered; the earliest deadline survives later failures and is checked again just before sending. After it, the post is marked uncertain.
  • A lock file another process just created is given a moment to be written and is taken if it's released meanwhile, instead of reported as unreadable.
  • Webhook replies that claim to be JSON but don't parse are logged as a warning.
  • rules.requireAlt makes missing alt text on image posts an error; postline doctor checks brand colours against WCAG AA contrast as the base card draws them, and lists colours it can't read.

An end-to-end run against a real brand fixed the gaps it found:

  • Drafts from postline plan lint with warnings instead of errors (a mistyped status stays an error), and render skips drafts unless one is named or --drafts is passed.
  • Token CSS variables keep their own spelling as well as lowercase, so Primer's var(--fgColor-default) works; two tokens that map to the same CSS variable are now a config error; the renderer warns when card content doesn't fit.
  • A post that has used up maxAttempts says what happened and how to retry; the same finding on several channels is reported once, without repeating channel names.

Docs and changelog updated; version bumped to 0.3.2.

Fixes from an end-to-end trial

postline was run end to end for a real brand: GitHub's changelog, with Primer's published design tokens (unmodified), Mona Sans, a custom layout, four channels (files and a signed webhook for real; LinkedIn and Bluesky without credentials), alerts, and the PR report. It found:

  • postline plan drafts failed lint with errors, which would fail the PR check. Declared drafts now get warnings; a mistyped status stays an error.
  • Token CSS variables were lowercased, so Primer's own var(--fgColor-default) silently matched nothing in a custom layout. Both spellings are now emitted.
  • render rendered placeholder drafts into the graphics library. Drafts are now skipped unless named or --drafts.
  • Long panel text pushed a landscape grid card into its footer with no warning. The renderer now warns when content doesn't fit, and the footer keeps its gap.
  • A post that had used up maxAttempts made publish exit 1 with no output. It now says what happened and how to fix it.
  • The same lint problem was listed once per channel, and errors repeated the channel name.