VaultFeed - full changelog

= 0.9.1 =
Fixes found on a live YouTube feed. Adds a small data migration (YouTube reconnect date only).

* Fix: YouTube, Vimeo, TikTok and Twitch videos now play in the lightbox and the post window, using each platform's official embedded player. They were given to a plain video element as a page address, which a browser cannot play. Nothing is requested from these services until a visitor clicks a video; the player address is built on the server from a sanitised ID and checked again in the script against a fixed host list. Other sources are unchanged.
* Fix: Bluesky videos play where the browser supports them natively and otherwise show a link to the post. No video library is bundled.
* Fix: closing the lightbox or post window now stops the video.
* Fix: a freshly connected YouTube account no longer shows "Expiring soon". The warning follows the refresh token, the access token is renewed automatically (before, a YouTube feed stopped loading about an hour after connecting and failed on every refresh), a revoked connection raises the reconnect warning, scheduled refreshes wait out a quota-exceeded answer, and 0.9.0 accounts are corrected on update.
* Fix: every connected account whose access token expires (YouTube, X, TikTok and others) is renewed before a feed is fetched; until now only the Google Reviews and Bluesky sources did this, so those feeds failed an hour or a day after connecting.
* Fix: a failed refresh no longer replaces good cached posts with an empty list ("No posts to display"); the last good copy keeps being shown.
* Fix: the feed list and Analytics show why a sync failed (HTTP code and the platform's reason). YouTube quota-exceeded answers pause scheduled refreshes until the pause ends.
* Fix: the hashtag shortcode no longer shows an empty page when its cached posts have just expired; it keeps showing the last cached posts for up to a day while a refresh is scheduled.
* Fix: the stale-feed warning no longer reports feeds that have never been fetched.
* Fix: the feed list shows the last sync time, and "Sync Now" on a feed that is not active says so.
* Fix: a failed YouTube code exchange now shows Google's reason (sanitised).
* Fix: a refresh that returned posts was recorded as a failed fetch whenever the Zapier listener ran (it was registered on a hook with the wrong number of arguments); this was a second cause of "failed 3 consecutive times". A failing listener can no longer fail a refresh.

= 0.9.0 =
Fixes found by independent audits of 0.8.9 and 0.8.10. No database changes.

* Security (WP.org review): the feed-import upload reader no longer carries any `phpcs:ignore` for a sanitisation rule. `$_FILES` fields are read on the line that sanitises them: the client file name with `sanitize_file_name( wp_unslash() )`, PHP's own temporary path with `filter_var( …, FILTER_UNSAFE_RAW )`, which returns it byte for byte so `is_uploaded_file()` still accepts it.
* Fix: the wizard's live preview no longer stays on "Loading..." for a feed on this site's own posts (its built-in account is never ticked) or when no account is ticked; it previews, or says what is missing.
* Fix: editing an aggregated feed no longer drops members that are paused, scheduled or in error; they are listed with their status and kept unless you untick them.
* Fix: editing a feed in the wizard keeps a "Show only" post-type list it has no button for (carousel, reel, text, link or three types) instead of turning it into photos or photos and videos, and no longer clears a stored minimum of 0, a stored date that is not plain YYYY-MM-DD or a stored schedule saved without a time zone (read as UTC).
* Fix: the legacy widget and the block editor keep a stored feed that is paused, scheduled or in error and label it with its name and status (before, the widget saved "no feed" and the block said "not available to you").
* Fix: an aggregated feed's RSS ETag and Last-Modified now include the members the reader may see, so hiding a post in a member, or pausing or restricting a member, changes the validators and a revalidating client gets the new feed instead of a 304. A shared cache may still serve its copy until it expires, so the aggregate's `Cache-Control: max-age` is now the shortest limit over the feed and its visible members (60 s for a moderated member, 300 s with hidden posts, never past a scheduled end), not just the aggregate's own settings.
* Fix: a legacy widget that holds a paused, scheduled, errored or restricted feed can be cleared again by choosing "— Select feed —"; before, the save put the old feed back.
* Security: the block editor lists the names of paused, scheduled or errored feeds only to users who can edit others' posts, and to anyone else only the feeds already used in the post they are editing (Contributors no longer see unpublished feed names).
* Fix: the wizard's live preview of an aggregated feed shows a translated explanation instead of a red untranslated "No adapter for source: aggregated".
* Fix: inactive members of an aggregated feed stay hidden in the wizard when they are filtered out (a CSS rule overrode the `hidden` attribute); the same rule is applied to the account and feed-type lists.
* Fix: the wizard shows a stored schedule that has no time zone as the UTC instant the server enforces, and an untouched save keeps stored dates (including those saved with a time) exactly as they were.
* Fix: the feed-import upload reader no longer alters PHP's temporary file path (double spaces and `%` sequences survived `sanitize_text_field()` badly); the client file name still goes through `sanitize_file_name()`.
* Performance: an aggregated feed's RSS reads its members' moderation versions in one query instead of one per member.
* Fix: What's New shows the current version badge and lists this release.

= 0.8.9 =
Supersedes the unreleased 0.8.6, 0.8.7 and 0.8.8 builds; everything below is new since 0.8.5.

* Fix: editing a feed in the wizard no longer resets anything the form did not change. The edit form now loads every field it posts (auto-block terms, alert e-mail, approval mode, alert switch, call-to-action, UTM, ticker speed, link overrides, GDPR gate and the rest) and posts an explicit 0 / empty for every unchecked box or cleared field, and the server changes only the fields the form actually sent. Display settings are overlaid on the stored ones the same way, so values set through REST or an import that the wizard has no input for (dark-mode preference, e-mail capture) survive an edit. The last aggregate member and the last injected card can be removed, and a field can still be cleared on purpose.
* Fix: the wizard can create and edit feeds for sources with no token row. Its account step is now built from the same per-source rule as activation: Mastodon (account row only), this site's own posts (a built-in "This site" account, no connection needed) and the tagged / mentions sources (the Instagram / X accounts). Before, those sources offered no account, so such a feed could not be created or edited in the wizard.
* Fix: the scheduled-feed cron (scheduled to active, expired to paused) now retires the cached feed lists; they stayed stale for up to 60 seconds on a persistent object cache. The list generation is a fresh token, not a counter, so an evicted counter can never revalidate an old list.
* Fix: the wizard's schedule fields now show and accept your browser's time zone and store UTC (the schedule fields were labelled UTC but a stored UTC time was displayed unchanged and a local time was saved as UTC). An untouched schedule is saved byte for byte as stored.
* Fix: `PUT /feeds/{id}` with a `filter_settings` object now merges the keys it names over the stored settings; before, an omitted `exclude_ids` un-hid every moderated post.
* Fix: the legacy widget and the block editor keep a stored feed the current user may not view (it was dropped when that user saved the widget or block).
* Fix: the RSS feed's Last-Modified and ETag now change when a post is hidden, restored, approved or rejected or the feed's visibility, schedule or filters are saved, so a conditional GET or shared cache cannot keep serving a moderated-away post; a feed in approval mode is shared for at most 60 seconds, one with hidden posts for 5 minutes.
* Security: every `vaultfeed/v1` REST response that depends on who asks (including `GET /feeds/{id}/posts`, which answers 404 or posts per viewer) now sends `Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private`. WordPress sends those headers on REST responses only to logged-in requests. The embed script and the signed image proxy are the same for every viewer and stay cacheable.
* Hardening: the verified Meta data-deletion payload is sanitised after the signature check; the feed-import upload fields are read through one helper that unslashes the client file name and keeps PHP's own temporary path as reported.

* Security: the RSS endpoint sent `Cache-Control: public, max-age=3600` on every response, replacing the no-cache headers WordPress sends to logged-in users, so a shared cache or CDN could store an administrator's copy of an aggregated or restricted feed and serve it to anonymous visitors for an hour. Responses to logged-in users, and every feed that is not public to everyone, are now private and not stored; a public feed read anonymously may be shared for up to an hour, never past its schedule, and varies by Cookie. The JSON Feed and embed page always send no-cache headers, `admin-ajax.php` sends them for every AJAX action, and the image proxy and embed script are the same for every viewer.
* Security: the block editor, the Elementor / Divi / Beaver Builder modules and the legacy widget listed every active feed in their feed pickers, so a Contributor opening the block editor learned the names and ids of editor-only and admin-only feeds. The pickers now offer only the feeds the user may view.
* Fix: saving a feed in the wizard replaced its filter settings with the form's fields; the form never carries the moderation list, the blocked accounts or the minimum-comments filter, so every edit un-hid every moderated post and cleared the blocklist. The wizard now starts from the stored settings and changes only the fields it shows.
* Fix: hiding or restoring a post, toggling approval mode, pinning posts and saving the blocklists on an active feed whose source stores no token (Mastodon, this site's own posts) or reads another source's token (Instagram tagged, X mentions) failed with a 500, because every save re-ran the activation check against the token table. The connected-account check now runs only when a save takes a feed live or moves it to another account, follows each source's own rule, and is answered as a 400 with the reason everywhere — including `wp vaultfeed feed create`, which used to stop with an uncaught exception.
* Fix: "Editors and above" visibility now means the Editor role and above (`edit_others_posts`); earlier builds tested `edit_posts`, which Contributors and Authors also hold.
* Fix: feed lists read with a limit (the block editor's) were not invalidated on save and could be served up to 60 seconds stale under a persistent object cache.
* Fix: RSS and JSON Feed autodiscovery links in `<head>` are emitted only for feeds that are public to everyone, so a page cache warmed by a logged-in reader never advertises a restricted feed.
* Fix: a database insert failure is reported in plain language in the wizard, Duplicate, REST and WP-CLI instead of the raw `db_insert_failed` code; two repository messages are translatable.
* Uninstall: also removes the network settings option (the multisite sitemeta row included) and the migration wizard's per-user "dismissed" flags.
* Network settings: the "share feeds across network" and "share tokens across network" switches were stored but read by nothing; both are removed from the Network Admin screen (the allowed-sources list and the subsite override remain).
* Note: feed schedules are stored in UTC. A schedule saved by a build before 0.8.6 in the site's local time is left as stored (it is not rewritten); re-enter it in the wizard if it is off. The wizard converts between your browser's time zone and UTC.
* Security: on sites with a persistent object cache (Redis, Memcached — common on managed hosts) a feed's rendered HTML was cached for 60 seconds under the feed id alone, so after an administrator viewed an aggregated feed, a copy that included its admin-only member feeds was served to anonymous visitors through the shortcode, block, widget, embed page and page builders. Rendered HTML now lives in a request-scoped (non-persistent) cache group and is keyed by viewer class and by the member feeds that viewer may see.
* Security: the `[vaultfeed_hashtag]` shortcode read the cached posts of every active feed, ignoring each feed's visibility (logged-in / editor / admin), schedule, moderation list and approval mode. A post now appears only if the feed it came from would show it to the current visitor.
* Security: an aggregated feed shows a member feed's posts only to visitors who could view that member feed directly — a member restricted to logged-in users, editors or admins, paused, or outside its schedule contributes nothing to anonymous visitors. Only feeds whose source is "Aggregated" merge other feeds; a member list on a platform feed is dropped.
* Security: `GET /wp-json/vaultfeed/v1/feeds/{id}/posts` returned a restricted feed's name, source and posts to anonymous callers. It now applies the same visibility gate as every other public surface and answers 404 (indistinguishable from a nonexistent feed). An API key identifies a client for rate limiting; it does not make a restricted feed public.
* Activating a feed (bulk action, REST API, WP-CLI) or saving it active now checks that its account is really connected on this site, not just that an account id is present — an imported feed keeps the account id of the site it was exported from — and refuses with a clear message otherwise. Every imported feed is now created paused — 0.8.5's admin-post import handler and format-1 files still created active feeds; connect the account under VaultFeed → Accounts, then activate.
* Imports and stored settings: display numbers are clamped to the wizard's ranges wherever a feed is read — import files, REST updates and feeds already stored — so a stored 100 posts per page renders as 50, and an auto-refresh interval the wizard does not offer runs at the nearest offered one (120 s becomes 60 s; nothing polls faster than 30 s) instead of switching auto-refresh off. Nested values where text is expected are ignored instead of raising PHP notices. Imported schedules are stored in UTC in both the JSON copy and the indexed columns the scheduler reads (a "10:00+02:00" schedule used to be stored two hours late, and a non-ISO date used to restrict the renderer but never reach the scheduler). An aggregated feed's member list is capped at 50 everywhere it is read and the members are loaded in one query. Partial imports report which feeds were skipped and why, in plain language.
* REST: creating or cloning a feed that fails validation (for example an empty or unconnected account) returns 400 with the reason instead of a 500; the batch endpoint no longer echoes raw request values back in `failed`.
* Fix: link-in-bio profiles never showed their embedded feed (the profile passed the feed as `feed_id`, the shortcode reads `id`).
* Fix: the block's "email capture after post" attribute is clamped to 1–100 like the stored setting; feed validation messages are translatable; double-escaped characters in import error messages.

= 0.8.5 =
* Fix wizard deprecation notices, aggregated feed save, export/import round trip and import error handling.

= 0.8.4 =
* Security (WP.org review): every request value is now sanitised at the point it is read with the WordPress function for its type, with no sanitisation finding suppressed. Feed imports (file upload, the Import Feed button and WP-CLI) validate the file with `is_uploaded_file()`, a 1 MB limit and a `.json` extension / MIME check, then size- and depth-limit the JSON and sanitise every decoded field before use. Injected cards are submitted as separate position / HTML / repeat fields, limited to 50 cards and a fixed HTML tag whitelist (no scripts, event handlers or `javascript:` links), and filtered again when rendered.
* Behaviour change (intended): existing injected cards that used tags outside that whitelist — for example `table`, `video`, `iframe`, `form` or `section` — now have those tags stripped when the feed renders. Edit the card in the Feed Wizard if it relied on them. URL fields (link-in-bio links, webhook URLs, hashtag-to-URL maps) now keep percent-encoded characters intact.

= 0.8.3 =
* WP.org review: removed a third-party review-site integration (source, account connection, and all related code) that WordPress.org does not permit for plugins listed on WordPress.org. Feed source count is now 20. If you had connected an account for it, that connection and any feeds built on it are simply gone — nothing to clean up.

= 0.8.2 =
* Security: fixed a cross-site scripting hole in the optional social-proof popup, which inserted a post's text, image URL and permalink into the page without escaping them. On hashtag, tagged-post, mention and review feeds that content is authored by third parties, so it could carry markup. All three values are now escaped, and every post URL the front-end script puts in an `href` or `src` must be a plain http(s) link, so a `javascript:` or `data:` URL from a platform response can no longer be used.
* Security: fixed a stored cross-site scripting hole in the review schema (JSON-LD) output. Review text and reviewer names come from the public on Google and Yelp, and were encoded in a way that let a review containing a closing `</script>` tag end the JSON-LD block and run as markup on your page. `<`, `>` and `&` are now hex-escaped inside the block; search engines read the data exactly as before.
* Security: the Meta data-deletion callback (`/vaultfeed/v1/fb-delete`) wrote a new row to the options table for every request it received, including unsigned and forged ones, so an anonymous caller could grow the database without limit. The request's Meta signature is now verified before anything is stored, unverified requests are refused outright, the route is rate limited, and old deletion logs are pruned after your data-retention period.
* Security: the Embed SDK script — which runs on the third-party sites you embed feeds into — now only accepts plain http(s) URLs for post links and images, so a `javascript:` URL in a platform response cannot run on the host page.
* Security: the public email-capture endpoint (`/vaultfeed/v1/subscribe`) accepted submissions even on sites that had never switched email capture on, writing addresses to the database or forwarding them to a configured Mailchimp/ConvertKit account. It now only accepts a sign-up for a feed the visitor is allowed to see and that has the email-capture form enabled, and is rate limited.
* Security: fixed an XML injection in the RSS feed — a post whose text contained the literal sequence `]]>` closed the description's CDATA section early, letting the rest of the post be parsed as raw markup by feed readers.
* Security: every public surface that can expose a feed — the iframe embed page, the RSS feed, the JSON Feed endpoint, the oEmbed provider and the click/impression tracking endpoints — now goes through one shared access gate (`FeedLoader::is_viewable()`) that honours the feed's status, its configured visibility (public / logged-in / editors / admins) and its scheduled window. Previously some of these surfaces checked only that the feed was active, so a feed restricted to logged-in users or admins could still have its name and embed URL disclosed. A `vaultfeed_feed_is_viewable` filter is available for custom rules.
* Security: the oEmbed endpoint now resolves only embed URLs belonging to this site (it previously accepted any URL carrying a `vaultfeed_embed` query variable, including URLs on other hosts), returns the same 404 for every rejection so responses cannot be used to tell which feed IDs exist, and is rate limited to 30 requests per IP per minute.
* WP.org review: upgraded the bundled SortableJS from 1.15.2 to 1.15.7 (latest stable) and shipped its MIT licence alongside it at `assets/js/vendor/sortable-LICENSE.txt`.
* WP.org review: the plugin header was missing its `License URI`, and the packaged `composer.json` still declared `"license": "proprietary"` next to a GPL plugin header. The header is complete, the package now declares GPL-2.0-or-later throughout, and the full licence text ships as `LICENSE.txt`.
* Change: AI caption rewriting now uses WordPress's built-in AI Client (WordPress 7.0+) exclusively — it sends the caption to whichever AI provider you have connected in WordPress. VaultFeed no longer contacts any AI provider directly and stores no AI key. The feature is available only when you have connected an AI provider in WordPress; the "Enhance Caption" button is hidden otherwise.
* New: the Social Proof Popup can now actually be switched on — it has a section under VaultFeed → Settings (feed, corner, delay). The feature shipped before without any settings screen, and its script could not load a post; both are fixed, and it now works on pages that have no feed on them.
* Fix: clicking "Save Settings" saved your changes but then left a half-rendered screen with a "headers already sent" warning, because the form was processed after the page had started printing. It is now processed before any output.
* Fix: the Welcome / onboarding screen lacked WordPress's standard page wrapper, so admin notices from WordPress and other plugins were placed at an arbitrary point on it. They now appear above the welcome card.
* Fix: on sites using plain permalinks the Moderation screen could not load any posts (its request URL contained two `?`).
* Fix: on page-cached sites the nonce-refresh endpoint now also renews the click-tracking nonce, and its response is marked `no-store` so a cache or CDN can never serve a stale one. It is rate limited.
* Fix: the email-capture form never actually submitted — the front-end script was not given the REST URL, and sent a nonce WordPress rejects. Sign-ups now work for visitors and logged-in users, including on page-cached sites.
* Fix: the hourly clean-up job queried a column that does not exist (`logged_at` instead of `created_at`), so it logged a database error on every run and the moderation log was never pruned.
* Fix: the legacy JSON Feed address (`?vaultfeed_json_feed=1&feed_id=N`) always served feed 1 regardless of `feed_id`. Also stopped registering an unprefixed public `feed_id` query variable, which could collide with other plugins.
* Fix: uninstalling on a multisite network overwrote WordPress's `$blog_id` global while looping over subsites.
* Fix: RSS items for posts with no author handle rendered an empty `(@)` after the author name.
* Housekeeping: `wp_json_encode()` and `rawurlencode()` in place of their plain PHP counterparts, and the last two ungated `error_log()` calls now only write when `WP_DEBUG` is on.
* Docs: documented the social platform media CDNs under "External services" — feed images and videos are served from the platforms' own CDNs to the visitor's browser (or through your own server if you enable the image proxy) — and corrected the third-party libraries note, which previously read as though no images at all were loaded from external servers.

= 0.8.1 =
* WP.org review: documented every previously-undisclosed optional third-party service in the "External services" section — Mailchimp and ConvertKit/Kit (email capture), the Google Analytics 4 Measurement Protocol (click analytics), Slack and Zapier webhooks (notifications/automation), the Shopify Storefront API (shop badge), and user-defined outbound webhooks. Each is opt-in and inactive unless you enter your own credentials or URL. Also added the missing Instagram privacy-policy link and example Mastodon instance terms.
* WP.org review: corrected a review-platform integration's terms and privacy URLs, which now pointed to its current legal pages (the previous paths returned 404), and replaced three other dead links (Yelp developer portal, and two links to a not-yet-live domain).
* WP.org review: SortableJS is now bundled with the plugin (`assets/js/vendor/`) instead of being loaded from the jsDelivr CDN on the Moderation screen. The plugin no longer loads any asset from an external server.
* Docs: corrected the readme's description of token-salt storage — the salt lives in the `vaultfeed_token_salt` database option; the plugin does not write to `wp-config.php` (that behaviour was removed in 0.2.1).
* Security: the image-proxy REST endpoint now has a real permission callback — it responds only when the proxy is enabled and the requested URL carries a valid server-issued HMAC signature, so it can no longer be used as an open proxy.
* Security: the AJAX "load more" endpoint now enforces the same feed visibility (public / logged-in / editor / admin) and schedule-window rules as the initial render.
* Security: the public impression and click-tracking endpoints now reject feed IDs that do not exist or are not active, so they cannot be used to create counter rows for arbitrary IDs.
* Security: feed-import uploads are now validated with `is_uploaded_file()` and each `$_FILES` field is sanitised individually.
* Fix: per-feed API-key restrictions were never enforced on `/feeds/{id}/posts` because the permission check read the wrong request parameter.
* WP.org review: everything the plugin stores now uses the distinctive `vaultfeed_` prefix instead of the two-letter `vf_` — all 11 database tables, the impression-counter options, every transient, the notice-dismissal user meta, a nonce action, and the JavaScript globals (`vfConfig` → `vaultfeedConfig`, etc.). Existing sites are migrated automatically and losslessly (database migration 0007: tables are renamed in place, options and user meta are renamed, short-lived transients are discarded). Only VaultFeed's own explicitly-listed names are touched.
* Fix: uninstall now drops VaultFeed's tables from an explicit list. It previously dropped every table matching `{prefix}vf_%`, which could have removed another plugin's tables that happened to share the two-letter prefix.
* Fix: pending database migrations now run on the first request of any kind after an update, not only on the next wp-admin visit, so the front end never queries a not-yet-migrated schema after a background auto-update.
* Security: rate limiting and click/impression de-duplication now identify visitors by `REMOTE_ADDR` only. The `X-Forwarded-For` and `CF-Connecting-IP` headers — which any client can forge to obtain a fresh rate-limit bucket — are honoured only if you declare a trusted proxy under VaultFeed → Settings → Reverse Proxy / CDN. **If your site is behind Cloudflare or another reverse proxy, set this after updating**, otherwise all visitors share the proxy's address for rate-limiting purposes. A `vaultfeed_client_ip` filter is available for custom set-ups.
* Security: Twitch and Snapchat bearer tokens, and a newly-created API key awaiting its one-time display, are now encrypted (AES-256-GCM) before being cached in a transient, matching the Reddit fix above. Any plaintext token cached by an older build is discarded on upgrade.
* Fix: three REST routes — the Embed SDK loader (`/embed.js`), the email-capture endpoint (`/subscribe`) and the image proxy (`/image`) — were never actually registered, because each added its `rest_api_init` callback from inside that same hook. The Embed SDK and the email-capture form now work.
* Fix: the Settings screen raised "Undefined array key" PHP warnings under `WP_DEBUG` on sites that had not saved settings yet.
* Fix: the admin-bar "VaultFeed" links and the feed-error notification email pointed at a non-existent admin page and produced a "not allowed" error.
* i18n: regenerated `languages/vaultfeed.pot` (it had fallen out of date and was missing over half of the plugin's strings).
* Security: the front-end click-tracking AJAX endpoint now returns an explicit error (never a misleading "success") when the security nonce is missing or invalid.
* Security: removed the redundant unauthenticated REST route `POST /feeds/{id}/posts/{platform_id}/click`. Click tracking is handled solely by the nonce-protected `admin-ajax` endpoint, closing an endpoint that allowed arbitrary click-counter increments.
* Security: the Meta (Facebook/Instagram) data-deletion callback now purges cached content only when the `signed_request` signature is cryptographically verified against the stored app secret; unverifiable requests are acknowledged (per Meta's spec) but take no privileged action.
* Security: the image proxy now fetches through `wp_safe_remote_get()` (blocking private/reserved IPs on every redirect hop) and enforces a 5 MB response-size cap before caching, mitigating SSRF and unbounded-cache abuse.
* Security: the Reddit app-only OAuth bearer token is now encrypted (AES-256-GCM) before being cached in a transient, rather than stored in plaintext.
* Security: display, filter, and cache settings are now sanitised per-field when hydrated from imported/uploaded JSON (`from_json`) — text via `sanitize_text_field`, URLs via `esc_url_raw`, custom CSS via `wp_strip_all_tags`, CSS lengths and colours against strict patterns, emails via `sanitize_email`, injected-card HTML via `wp_kses_post`, and JSON maps decoded and re-encoded with typed values.
* WP-CLI: `wp vaultfeed feed export` now always writes into the plugin's folder inside `wp-content/uploads` (resolved at runtime via `wp_upload_dir()`); the `--file` option is treated as a filename only, never an arbitrary path.

= 0.8.0 =
* Housekeeping: versioning reset to 0.8.0 ahead of a 1.0 release (X.Y.Z, each of Y and Z rolling over to the next level after 9 — e.g. 0.8.9 → 0.9.0, 0.9.9 → 1.0.0). Prior internal builds were numbered 0.1.0–0.45.x.
* Fix: two activation-fatal bugs — a stale Composer autoloader referencing dev-only packages, and Elementor/Divi/Beaver Builder integrations that errored on sites without those page builders installed.
* Fix: WP-CLI command conflict between `wp vaultfeed` and its subcommands; renamed to `wp vaultfeed info`.
* Fix: cron scheduling deferred to `init` in 5 classes to avoid a false "translation loaded too early" notice on WordPress 6.7+.
* WP.org review: resolved remaining Plugin Check findings — output escaping, input sanitization, SQL preparation, discouraged functions, i18n, and nonce checks across ~35 files.
* Pro: removed the "no separate Meta developer account" option — Facebook/Instagram now always use your own Meta app, like every other platform. ReelVault, DHT storage, and priority support remain in Pro.
