Two mistakes in the previous commit, both of which broke CI.
The lockfile was not regenerated after adding puppeteer-core to
devDependencies, and `npm ci` requires the two to agree — so every job that
installs dependencies failed before running anything.
The smoke test was also placed in test/, which I described as keeping it out of
`npm test`. It does not: `node --test` globs that directory, so the runner
picked it up regardless of intent, tried to drive a browser as a unit test, and
failed. It now lives beside the server as smoke-ui.js, with a note saying why,
so the next person does not put it back.
Verified the way it should have been the first time: npm ci succeeds, native
modules still load, npm test is 807/807 with no browser involved, and
`npm run smoke` is 32/32 on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
sharp decodes uploaded files directly (lib/content-ingest.js, routes/content.js
both call sharp(file.path) on whatever a user uploaded), so its bundled libvips is
part of the request path rather than a build-time detail. Moves 0.33.5 -> 0.35.3,
libvips 8.15 -> 8.18.
Validated against the calls this codebase actually makes, because it is a major
bump: metadata() still reports EXIF orientation (1/3/6/8 all round-trip, which is
what lib/media-orientation.js exifSwapsWH and the rotation-aware dimensions depend
on), a bare .rotate() still auto-orients, and resize().jpeg().toFile() is unchanged.
png/webp/jpeg/gif/avif all still encode and decode, and malformed input still throws
rather than crashing.
The new libpng is stricter, which surfaced a latent problem in the AUTH-01 test: its
1x1 PNG literal had a corrupt IDAT chunk whose stored CRC did not match its data. The
old decoder accepted it; the new one refuses with "vipspng: libpng read error", so no
thumbnail was written and the content-gate assertions failed with a 404 that reads
like an auth regression. Replaced with a PNG whose every chunk CRC verifies. The
stricter decode is the correct behaviour and is kept.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran `npm audit fix` (no --force) in server/. Only transitive dependencies
moved, within existing semver ranges — package.json is unchanged, so this is
purely a package-lock.json update:
ws 8.18.3 -> 8.21.1, qs -> 6.15.3, body-parser -> 1.20.6,
engine.io -> 6.6.9, js-yaml -> 4.3.0, plus express/socket.io sub-deps.
Vulnerabilities: 13 (6 moderate, 7 high) -> 2 high.
Verified: full server suite 556/556 (incl. socket-handler + reconnect-storm
tests that exercise ws); boot smoke OK (server starts, /api/version responds,
socket.io/engine.io handshake returns 200).
Left for a separate, deliberate change (both need breaking major bumps):
- nodemailer 6 -> 9 (email send API; several CRLF/SSRF advisories)
- sharp 0.33 -> 0.35 (libvips CVEs; thumbnail/image path)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a pluggable email transport so self-hosters without Azure/M365 can send
mail through any standard SMTP server (Postfix, Gmail, Mailgun, SendGrid, corp
relay). Graph stays the default; behavior is byte-for-byte unchanged when
EMAIL_TRANSPORT is unset or "graph".
- config: EMAIL_TRANSPORT ("graph"|"smtp", default graph) + SMTP_HOST/PORT/
SECURE/USER/PASSWORD/FROM.
- services/email.js: branch by transport behind the SAME public sendEmail()/
isConfigured() surface. SMTP via nodemailer (lazy-required, like MSAL).
Shared across both transports: the "[ScreenTinker] " subject prefix (unless
rawSubject), the GRAPH_DEV_RESTRICT_TO allow-list, html-from-text derivation,
and the never-throws contract (failures log + return sent:false). SMTP_SECURE
true=implicit TLS(465)/false=STARTTLS(587). Auth optional (unauthenticated
relay ok); SMTP_USER without SMTP_PASSWORD is flagged. SMTP_FROM parses
"Name <addr>". New emailConfigStatus() for startup diagnostics.
- server.js: startup logs the transport and a LOUD error when the selected
transport is partially configured (some fields set, others missing) or when
EMAIL_TRANSPORT is invalid (falls back to graph). A fully-unset transport
stays a silent stdout fallback (unchanged dev behavior).
- nodemailer ^6.9.16 added as a production dep (bundled in the Docker image).
- .env.example + README: SMTP config section, Gmail example, transport table.
- test/email-transport.test.js: 15 tests — transport selection, config
validation (missing/partial/invalid), SMTP message building (from/prefix/
fromName override/text alt), sendEmail routing (mocked nodemailer), rawSubject,
dev-restrict on smtp, and the smtp_error never-throws path.
462/462 server tests pass. Boot verified for all four states (configured,
misconfigured, invalid, default).
Closes#173
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>