=== VaultFeed ===
Contributors: theteamccgnomes
Tags: social media, instagram, facebook, social feed, privacy
Requires at least: 6.4
Tested up to: 7.1
Stable tag: 0.9.0
Requires PHP: 8.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Self-sovereign social feeds from 20 sources, tokens encrypted on your own server — never uploaded to a third party.

== Description ==

**VaultFeed** displays social media feeds on your WordPress site while keeping your OAuth tokens encrypted on your own server — never uploaded to a third-party SaaS platform.

Connect accounts from **20 feed sources**: **Facebook**, **Instagram**, **Twitter / X**, **YouTube**, **TikTok**, **Threads**, **LinkedIn**, **Bluesky**, **Pinterest**, **Mastodon**, **Reddit**, **Snapchat**, **Twitch**, **Vimeo**, **Google Business Profile**, **Yelp**, and more. Every token is AES-256-GCM encrypted and stored only in your own database.

= Features =

VaultFeed is fully functional with no artificial limits on feed count, post count, or layout — everything below is included with the plugin as distributed on WordPress.org:

* Connect accounts from **20 feed sources**: Facebook Pages, Instagram Business / Creator, Twitter / X, YouTube, TikTok, Threads, LinkedIn, Bluesky, Pinterest, Mastodon (public accounts, no credentials needed), Reddit, Snapchat (Snap Kit Display API), Twitch (clips and VODs via Helix API), Vimeo (user/channel/group feeds), Google Business Profile, Yelp, and more
* Display photo, video, reel, and **review** feeds in **Grid, List, Masonry, Carousel, Slider, Waterfall, Ticker, Timeline,** or **Wall** layout
* Star-rating widget with WCAG-compliant aria-label for review feeds
* Configurable columns (1–6), aspect ratio, hover effects, and post count
* Lightbox for photos and videos with keyboard navigation
* **Load More** button and infinite scroll
* Feed header with optional follow button
* Shortcode `[vaultfeed id="N"]` and Gutenberg block
* Automatic content caching with configurable TTL, fully adjustable
* Caption display, source icon, date, and like / comment counts
* Optional "Powered by VaultFeed" attribution link — off by default, opt-in only
* Fully translatable — `.pot` file included
* WCAG 2.1 AA compliant — focus traps, live regions, 4.5:1 contrast
* Zero telemetry — nothing phones home

= Pro add-on =

A separately-sold Pro add-on is available from [the VaultFeed website](https://ccgnomes.com/vaultfeed/) and is **not** distributed via WordPress.org — none of its code is included in this plugin. It adds functionality that depends on infrastructure this plugin does not include or run:

* ReelVault source — a video feed served from your own self-hosted Holochain node (requires running that node yourself; VaultFeed does not bundle or install it)
* DHT token storage as an alternative to the local database storage described below
* Priority support

None of the above is locked, disabled, or hidden in the WordPress.org version — it simply isn't present, because it depends on external infrastructure (a Holochain node) that this plugin doesn't ship or manage.

= How token storage works =

When you connect an account, VaultFeed encrypts the OAuth token with AES-256-GCM and stores it in your own WordPress database. The encryption key is derived from your WordPress secret keys and a random per-installation salt that is generated at first activation and stored in your WordPress database (the `vaultfeed_token_salt` option). VaultFeed never writes to `wp-config.php` or to any WordPress core, plugin, or theme file. No token ever leaves your server.

= External services =

VaultFeed relies on third-party services to do its job: it fetches feed content from the social platforms you explicitly connect, and it can optionally send data to a small number of integrations (email marketing, analytics, notifications, store lookups, AI caption rewriting) that stay completely inactive until you enter your own credentials or URL for them. Every service the plugin can contact is listed below with what is sent, when, and its terms and privacy policy. No data is ever sent to VaultFeed or its authors; the plugin operates no background service and has no telemetry.

1. **Facebook Graph API** (`graph.facebook.com`) — when you initiate an OAuth connection from the Accounts page, and when fetching feed content. Governed by [Meta's Terms of Service](https://www.facebook.com/terms.php) and [Privacy Policy](https://www.facebook.com/policy.php). Only called when you have a connected Facebook account.

2. **Instagram Graph API** (`graph.instagram.com`, `graph.facebook.com`) — when you initiate an Instagram OAuth connection from the Accounts page, and when fetching Instagram feed, tagged-post, or story content. Governed by [Meta's Terms of Service](https://www.facebook.com/terms.php) and [Instagram's Privacy Policy](https://privacycenter.instagram.com/policy). Only called when you have a connected Instagram account.

3. **Twitter / X API v2** (`api.twitter.com`) — when you initiate a Twitter OAuth 2.0 PKCE connection, and when fetching tweet content. Governed by [X's Terms of Service](https://x.com/en/tos) and [Privacy Policy](https://x.com/en/privacy). Only called when you have a connected Twitter account.

4. **Google / YouTube Data API v3** (`accounts.google.com`, `oauth2.googleapis.com`, `www.googleapis.com`) — when you initiate a Google OAuth 2.0 connection (your browser is sent to Google's consent screen), and when fetching YouTube channel or playlist content. Governed by [Google's Terms of Service](https://policies.google.com/terms) and [Privacy Policy](https://policies.google.com/privacy). Only called when you have a connected YouTube account.

5. **TikTok for Developers API v2** (`www.tiktok.com`, `open.tiktokapis.com`) — when you initiate a TikTok OAuth 2.0 connection, and when fetching video content. Governed by [TikTok's Terms of Service](https://www.tiktok.com/legal/page/row/terms-of-service/en) and [Privacy Policy](https://www.tiktok.com/legal/page/row/privacy-policy/en). Only called when you have a connected TikTok account.

6. **Threads API** (`graph.threads.net`) — when you initiate a Threads OAuth 2.0 connection, and when fetching posts. Governed by [Meta's Terms of Service](https://www.facebook.com/terms.php) and [Privacy Policy](https://www.facebook.com/policy.php). Only called when you have a connected Threads account.

7. **Google Business Profile APIs** (`mybusiness.googleapis.com`, `mybusinessaccountmanagement.googleapis.com`, `mybusinessbusinessinformation.googleapis.com`) — when listing your business accounts/locations and when fetching Google business reviews. Uses the same Google OAuth credentials as YouTube (item 4 above); both APIs must be enabled in your Google Cloud project. Governed by [Google's Terms of Service](https://policies.google.com/terms) and [Privacy Policy](https://policies.google.com/privacy). Only called when you have a connected Google Business Profile account.

8. **Yelp Fusion API** (`api.yelp.com`) — when you verify a Yelp API key connection, and when fetching business reviews. Governed by [Yelp's Terms of Service](https://www.yelp.com/static?p=tos) and [Privacy Policy](https://www.yelp.com/static?p=privacy). Only called when you have a connected Yelp account.

9. **LinkedIn REST API** (`api.linkedin.com`) — when you initiate a LinkedIn OAuth 2.0 connection, and when fetching posts. Governed by [LinkedIn's User Agreement](https://www.linkedin.com/legal/user-agreement) and [Privacy Policy](https://www.linkedin.com/legal/privacy-policy). Only called when you have a connected LinkedIn account.

10. **Bluesky AT Protocol API** (`bsky.social`, `public.api.bsky.app`) — when you connect a Bluesky account via App Password, and when fetching posts. Governed by [Bluesky's Terms of Service](https://bsky.social/about/support/tos) and [Privacy Policy](https://bsky.social/about/support/privacy-policy). Only called when you have a connected Bluesky account.

11. **Pinterest API** (`api.pinterest.com`) — when you initiate a Pinterest OAuth 2.0 connection, and when fetching boards and pins. Governed by [Pinterest's Terms of Service](https://policy.pinterest.com/en/terms-of-service) and [Privacy Policy](https://policy.pinterest.com/en/privacy-policy). Only called when you have a connected Pinterest account.

12. **Mastodon API** (`{your-instance}/api/v1/`) — when you look up a Mastodon account by handle, and when fetching public statuses. The specific domain depends on the Mastodon instance you connect, and each instance publishes its own terms and privacy policy — for example, mastodon.social's [Terms of Service](https://mastodon.social/terms-of-service) and [Privacy Policy](https://mastodon.social/privacy-policy). Only called when you have a connected Mastodon account.

13. **Reddit OAuth2 / REST API** (`oauth.reddit.com`, `www.reddit.com`) — when you initiate a Reddit OAuth 2.0 client-credentials connection, and when fetching subreddit posts. Governed by [Reddit's User Agreement](https://www.redditinc.com/policies/user-agreement) and [Privacy Policy](https://www.reddit.com/policies/privacy-policy). Only called when you have a connected Reddit account.

14. **Snapchat Login Kit / Snap Kit Display API** (`accounts.snapchat.com`, `kit.snapchat.com`) — when you initiate a Snapchat OAuth 2.0 connection and exchange or refresh its token, and when fetching your profile and public stories. Governed by [Snapchat's Terms of Service](https://www.snap.com/en-US/terms) and [Privacy Policy](https://www.snap.com/en-US/privacy/privacy-policy). Only called when you have a connected Snapchat account.

15. **Twitch Helix API** (`id.twitch.tv`, `api.twitch.tv`) — when you connect a Twitch app (an app access token is requested from `id.twitch.tv`), and when fetching clips and VODs. Governed by [Twitch's Terms of Service](https://www.twitch.tv/p/legal/terms-of-service/) and [Privacy Policy](https://www.twitch.tv/p/legal/privacy-policy/). Only called when you have a connected Twitch account.

16. **Vimeo API** (`api.vimeo.com`) — when you connect a Vimeo account via Personal Access Token, and when fetching user, channel, or group video feeds. Governed by [Vimeo's Terms of Service](https://vimeo.com/terms) and [Privacy Policy](https://vimeo.com/privacy). Only called when you have a connected Vimeo account.

17. **AI caption rewriting — via the WordPress AI Client** — optional, and never automatic: it runs only when you click "Enhance Caption" on the Moderation screen, and sends only that one caption's text plus a short rewrite instruction (the chosen tone). No OAuth tokens, visitor data, or other site data is ever included. VaultFeed hands the request to WordPress's built-in AI Client (WordPress 7.0 and later), so the text goes to whichever AI provider **you** have connected under WordPress's own AI settings; VaultFeed stores no credentials for it and contacts no AI provider on its own, and that provider's terms and privacy policy apply. If you have not connected an AI provider in WordPress, the feature is simply unavailable and nothing is sent anywhere.

18. **Mailchimp Marketing API** (`<dc>.api.mailchimp.com`) — used only by the optional email-capture feature. Only active if you enable email capture on a feed **and** enter your own Mailchimp API key and Audience (list) ID in VaultFeed → Settings. When a visitor submits the email-capture form, their email address is sent to Mailchimp to be added as a subscriber to your audience. Nothing is sent if a Mailchimp key is not configured. Governed by [Mailchimp's Terms of Use](https://mailchimp.com/legal/terms/) and [Privacy Policy](https://www.intuit.com/privacy/statement/).

19. **ConvertKit / Kit API** (`api.convertkit.com`) — used only by the optional email-capture feature. Only active if you enable email capture on a feed **and** enter your own ConvertKit (Kit) API key and Form ID in VaultFeed → Settings. When a visitor submits the email-capture form, their email address is sent to ConvertKit to subscribe them to your form. Nothing is sent if a ConvertKit key is not configured. Governed by [Kit's Terms of Service](https://kit.com/terms) and [Privacy Policy](https://kit.com/privacy).

20. **Google Analytics 4 Measurement Protocol** (`www.google-analytics.com`) — used only for optional click analytics. Only active if you enter your own GA4 Measurement ID and API Secret in VaultFeed → Settings. When a visitor clicks a post, a `vaultfeed_click` event (feed ID, post ID, and post type — no personal data) is sent to your Google Analytics property. Nothing is sent if GA4 credentials are not configured. Governed by [Google's Terms of Service](https://policies.google.com/terms) and [Privacy Policy](https://policies.google.com/privacy).

21. **Slack incoming webhooks** (`hooks.slack.com`) — used only for optional new-post notifications. Only active if you paste your own Slack incoming-webhook URL in VaultFeed → Settings **and** enable new-post alerts on a feed. When new posts arrive in that feed, a short text message (the number of new posts and the feed's name — no post content and no visitor data) is sent to your Slack webhook. Nothing is sent if no Slack URL is configured. Governed by [Slack's Terms of Service](https://slack.com/terms-of-service/user) and [Privacy Policy](https://slack.com/trust/privacy/privacy-policy).

22. **Zapier webhooks** (`hooks.zapier.com`) — used only for optional automation. Only active if you paste your own Zapier "Catch Hook" URL in VaultFeed → Settings. When a feed refresh finds new posts, the feed ID and, for each new post, its platform ID, caption text, first media URL, permalink, and timestamp are sent to your Zapier webhook. This is public social content you chose to display — no visitor data is included. Nothing is sent if no Zapier URL is configured. Governed by [Zapier's Terms of Service](https://zapier.com/legal/terms-of-service) and [Privacy Policy](https://zapier.com/privacy).

23. **Shopify Storefront API** (`{your-store}.myshopify.com`) — used only by the optional "Shop" badge. Only active if you enter your own Shopify store URL and Storefront API access token in VaultFeed → Settings and enable the shop badge on a feed. When a post's hashtag has no product link you mapped manually, that hashtag is sent to your own store's Storefront API as a product-handle lookup, together with your Storefront token, to find the product's URL (results are cached for one hour). No visitor data is sent. Nothing is sent if Shopify is not configured. Governed by [Shopify's Terms of Service](https://www.shopify.com/legal/terms) and [Privacy Policy](https://www.shopify.com/legal/privacy).

24. **Your own outbound webhook URLs** — optional. Only active if you add one or more webhook endpoints in VaultFeed → Webhooks. When an event you selected fires (feed refreshed, feed error, post approved/rejected), a JSON payload is POSTed to the URL(s) **you** entered: the event name, the feed ID, and for refresh events each post's platform ID, type, source, caption text (first 280 characters), permalink, and publish date. Requests are signed with HMAC-SHA256 using your webhook secret. The destination is whatever server you specify, so its terms and privacy policy are those of the service you choose; no data goes anywhere unless you add a URL.

25. **Social platform media CDNs** (for example `scontent.cdninstagram.com`, `pbs.twimg.com`, `i.ytimg.com` — the exact hosts depend on the platforms you connect) — the images, thumbnails and videos inside your feeds are served from the platform's own CDN, so a visitor's browser requests them directly from that platform while viewing the feed, exactly as an ordinary embedded post would. The URLs come from the platform API response; VaultFeed does not rehost them. The platform's terms and privacy policy are the ones listed for that platform above. If you would rather your visitors never contact those CDNs, switch on the built-in image proxy — `wp option update vaultfeed_image_proxy_enabled 1` — and your own server fetches and caches the images instead.

26. **WordPress.org update API** — standard WordPress plugin update check behaviour. No VaultFeed-specific data is transmitted beyond the plugin slug and current version.

= Third-party libraries =

VaultFeed's own scripts, styles and fonts are all bundled with the plugin — none are loaded from an external server or CDN. (The post images and videos *inside* your feeds are a separate matter: those are served from the social platforms' CDNs, as described in item 25 of "External services" above.)

* **SortableJS 1.15.7** (MIT licence) — drag-to-reorder on the admin Moderation screen. Bundled at `assets/js/vendor/sortable.min.js` with the unminified source alongside at `assets/js/vendor/sortable.js` and the upstream licence at `assets/js/vendor/sortable-LICENSE.txt`. Source: [github.com/SortableJS/Sortable](https://github.com/SortableJS/Sortable).
* All other minified files in `assets/js/*.min.js` and `assets/css/*.min.css` are built from the human-readable `.js` / `.css` files of the same name that ship next to them.

== Installation ==

1. Upload the `vaultfeed` folder to `/wp-content/plugins/`
2. Activate the plugin via **Plugins → Installed Plugins**
3. Go to **VaultFeed → Settings** and enter your app credentials for the platforms you want to use
4. Go to **VaultFeed → Accounts** and connect your accounts
5. Go to **VaultFeed → Feeds** and click **+ New Feed** to create your first feed
6. Copy the shortcode (e.g. `[vaultfeed id="1"]`) and paste it into any post, page, or widget

= Requirements =

* WordPress 6.4 or later
* PHP 8.1 or later
* Developer credentials for each platform you wish to connect:
  * **Facebook / Instagram** — a Meta Developer App with `pages_read_engagement` and `instagram_basic` permissions
  * **Twitter / X** — a Twitter Developer App with OAuth 2.0 enabled and scopes `tweet.read users.read offline.access`
  * **YouTube** — Google Cloud OAuth 2.0 credentials with the YouTube Data API v3 enabled
  * **TikTok** — a TikTok Developer App with `user.info.basic` and `video.list` scopes
  * **Threads** — a Meta Developer App with `threads_basic` permissions (separate from your Facebook / Instagram app)
  * **Google Business Profile reviews** — same Google Cloud project as YouTube, with the Business Profile APIs additionally enabled
  * **Yelp** — a Yelp Fusion API key from the [Yelp developer portal](https://docs.developer.yelp.com/)
  * **LinkedIn** — a LinkedIn Developer App with `openid profile email r_member_social w_member_social` scopes enabled
  * **Bluesky** — no developer account needed; create an App Password at bsky.app → Settings → App Passwords
  * **Pinterest** — a Pinterest Developer App with `boards:read`, `pins:read`, and `user_accounts:read` scopes
  * **Mastodon** — no credentials required; public accounts are accessible via the Mastodon public API

== Frequently Asked Questions ==

= Do I need developer accounts for the platforms I want to connect? =

Most platforms require their own developer app credentials, entered in **VaultFeed → Settings**. The setup takes 10–15 minutes per platform. Two exceptions: **Bluesky** uses an App Password (no developer account needed), and **Mastodon** uses public accounts with no credentials at all. The credentials each platform needs are listed under **Installation → Requirements** above, and each platform's section in **VaultFeed → Settings** links to the matching developer portal. For the exact OAuth callback URL and required scopes for each platform, plus troubleshooting, see the [VaultFeed setup guide](https://ccgnomes.com/vaultfeed/support/).

= Where can I get help? =

Setup, troubleshooting, and a support ticket form are on the [VaultFeed Setup & Support page](https://ccgnomes.com/vaultfeed/support/). You can also use the plugin's support forum here on WordPress.org.

= Where are my OAuth tokens stored? =

Tokens are stored in the `vaultfeed_tokens` table in your WordPress database, encrypted with AES-256-GCM. The encryption key is derived locally from your `AUTH_KEY` and a random salt stored in your WordPress database (VaultFeed does not write to `wp-config.php`). No token is ever transmitted to VaultFeed or any third party.

= Is there a limit on feeds, posts, or layouts in the free version? =

No. There is no cap on the number of feeds you can create, no cap on posts per feed, and all nine layouts (Grid, List, Masonry, Carousel, Slider, Waterfall, Ticker, Timeline, Wall) are available. The optional "Powered by VaultFeed" attribution link is off by default and only appears if you turn it on yourself in a feed's Display Settings.

= Does this work with WordPress multisite? =

Yes. Each sub-site manages its feeds and account connections independently.

= Is this GDPR-compliant? =

VaultFeed sends nothing to its authors and makes no unsolicited third-party network calls. By default it stores no visitor personal data: click and impression analytics are aggregate counters, and the visitor IP address is used only transiently (hashed, in short-lived rate-limit/de-duplication keys) and is never stored in readable form. If you turn on the optional **email capture** form, the addresses visitors submit are sent to the Mailchimp or ConvertKit account you configured — or, if neither is configured, kept in your own WordPress database — so you are the data controller for them and should disclose that in your Privacy Policy. Your wider GDPR obligations depend on the content of your social media feeds and your agreements with the respective platforms. See the "External services" section for exactly what each optional integration sends.

= Can I display the same feed on multiple pages? =

Yes — use the same shortcode `[vaultfeed id="N"]` anywhere on your site. The cached content is shared so it counts as one API call.

= Does the plugin support Instagram Stories? =

Yes (free tier). Stories are a feed type you can choose in the feed creation wizard, step 3.

= Is my data sent to VaultFeed servers? =

No. VaultFeed operates no background service and receives no data from your site. Out of the box, the plugin makes outbound HTTP calls only to the social platforms you explicitly connect (to fetch feed content) and to the WordPress.org API (to check for plugin updates). The optional integrations listed under "External services" (Mailchimp, ConvertKit, Google Analytics 4, Slack, Zapier, Shopify, and your own webhooks) contact those services only after you enter your own credentials or URL for them — and even then, the data goes to **your** account with that service, never to VaultFeed. Your OAuth tokens never leave your server.

= Does this work with caching plugins? =

Yes. VaultFeed maintains its own server-side cache in the `vaultfeed_feed_cache` database table, independent of any full-page cache. Feeds load correctly behind WP Super Cache, W3 Total Cache, WP Rocket, LiteSpeed Cache, and similar plugins. If you use a page cache with a very long TTL, enable **auto-refresh** in your feed's Display Settings so content stays current without a full page reload.

= What PHP version is required? =

PHP 8.1 or higher is required. PHP 8.2 and 8.3 are fully supported. WordPress 6.4 or higher is required.

= What happens if my token expires? =

VaultFeed checks token expiry and marks the account as "expiring" in the Accounts page. You can reconnect by clicking **Reconnect** on the account card. Feeds for that account are paused until the token is refreshed.

== Screenshots ==

1. The Feeds list page — all your feeds at a glance with shortcode copy.
2. Feed creation wizard — choose source, account, type, and display settings.
3. Live preview panel — see changes in real time as you configure.
4. Grid layout with hover overlay effect on the front end.
5. List layout with caption, date, and meta displayed.
6. Accounts page — connected accounts with token status badges.

== Changelog ==

= 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.

= 0.45.13 =
* WP.org review: fixed the free-build packaging scripts (`build/make-zip.py` and the `build-free` release job) so the WordPress.org zip no longer bundles directory screenshots or the Pro-only Holochain sidecar / DHT / ReelVault / license-validation code paths — that code now ships only in the separately-distributed Pro package, as already stated in this readme.
* WP.org review: moved all remaining inline `<script>`/`<style>` output in the admin UI to properly enqueued, conditionally-loaded assets (`wp_enqueue_script`/`wp_enqueue_style`) with dynamic values passed via `wp_add_inline_script`/`wp_localize_script`.
* Security: added nonce verification to the front-end click-tracking AJAX endpoint (`vaultfeed_track_click`), including in embedded/iframe views.
* Fix: Site Health's token-encryption-salt check no longer reports a false "critical" error — it now recognizes the salt stored in `wp_options` (current behaviour) in addition to the legacy `wp-config.php` constant.
* Docs: corrected readme claims about free-tier limits (feed count, post count, layout choice, forced attribution) that no longer reflected actual plugin behaviour — the WordPress.org edition has always been fully functional with no artificial caps.

= 0.45.12 =
* Fix: CacheSweep now calls EngagementMetrics::sweep_old_keys() on every hourly run so vf_imp_* impression options are pruned instead of accumulating indefinitely.
* Fix: Plugin::uninstall() now deletes vf_imp_* impression options, which were previously orphaned after plugin deletion because they use a different prefix than vaultfeed_*.

= 0.45.11 =
* Security: ajax_poll_new (nopriv AJAX) now enforces a 120 requests/minute rate limit per IP via RateLimiter.
* Security: ajax_poll_new known_ids array is capped at 500 entries to prevent memory exhaustion from oversized payloads.

= 0.45.10 =
* Fix: ApiKeyManager SELECT queries used backtick-quoted %i placeholder, silently breaking all API key authentication; fixed for validate() and list_all().
* Fix: vf_api_keys added to SiteHealthChecks EXPECTED_TABLES so a missing table is detected.
* Fix: FeedRepository::delete() now calls ApiKeyManager::remove_feed_from_all_keys() to remove stale feed ID references from API key scope metadata.

= 0.45.9 =
* Fix: CacheSweep data-retention DELETE queries were silently failing due to backtick-quoted %i placeholder producing double-backtick SQL; fixed for vf_feed_cache and vf_click_events.
* Fix: Added vf_moderation_log retention purge to CacheSweep (previously no purge existed for this table).
* Fix: FeedRepository::delete() now calls delete_option('vaultfeed_approvals_{id}') to remove orphan approval maps on feed deletion.
* Fix: vf_click_events added to SiteHealthChecks EXPECTED_TABLES so missing-table detection is complete.
* Fix: Removed stale wiring TODO comments from EmailCaptureController, ImageProxy, NotificationsPage, and AnalyticsCommand (all already wired).
* Fix: AnalyticsCommand SELECT queries used backtick-quoted %i placeholder; fixed to bare %i.
* Consistency: to_json() in CacheSettings, DisplaySettings, and FilterSettings now uses wp_json_encode() instead of bare json_encode().

= Earlier versions =
See the full version history in the plugin's changelog on GitHub.

== Upgrade Notice ==

= 0.9.0 =
Bug-fix release: live preview, aggregate members, stored filters, widget feeds, aggregate RSS caching, block-editor feed names and a cleaner upload reader. No database changes.

= 0.8.9 =
Security and bug-fix release: closes cached-render and RSS/REST cache leaks of restricted feeds and picker exposure, and the wizard no longer resets filter or display settings when you edit a feed. No database changes.

= 0.8.5 =
Bug-fix release: aggregated feeds save again, exported feeds import again (old export files included), and the wizard no longer logs PHP deprecation notices. No database changes.

= 0.8.4 =
Security hardening of admin input handling and feed import, per WordPress.org review. Injected cards are limited to a fixed HTML tag whitelist; cards using table, video, iframe, form or section tags will have those stripped when rendered. No database changes.

= 0.8.3 =
WP.org compliance release. Removed a third-party review-site integration per WordPress.org guidelines; feed source count is now 20. No database changes.

= 0.8.2 =
Security release. Fixes two cross-site scripting holes (review schema output, social-proof popup), an RSS injection, and an email-capture endpoint that ignored its on/off setting. Restricted feeds are now hidden from embed, RSS, JSON Feed and oEmbed too. No database changes.

= 0.8.1 =
Security and compliance release. Database tables are renamed automatically (vf_* to vaultfeed_*) with no data loss. If your site is behind Cloudflare or a reverse proxy, set VaultFeed → Settings → Reverse Proxy / CDN after updating.

= 0.44.0 =
Adds Snapchat, Twitch, and Vimeo source adapters. No database schema changes. Safe to update in place.

= 0.43.0 =
Adds API key management and Embed SDK. No database schema changes. Safe to update in place.

= 0.42.0 =
Adds CTA card injection and notification log. No database schema changes. Safe to update in place.

= 0.41.0 =
Adds Reddit, Instagram Tagged, and Twitter Mentions adapters. No database schema changes. Safe to update in place.

= 0.40.0 =
Adds Pinterest, WordPress CPT, Mastodon, Bluesky, LinkedIn, and Google Reviews adapters alongside the Moderation page. No database schema changes. Safe to update in place.

= 0.39.0 =
Adds Threads, TikTok, YouTube, and Twitter / X adapters, plus WP-CLI commands and RSS output. No database schema changes. Safe to update in place.

= 0.33.0 =
No database changes. Safe to update in place.

= 0.31.0 =
No database changes. Safe to update in place.

= 0.28.0 =
Adds an orphan-cache sweep that runs automatically during the next hourly cron. No manual action required. No database schema changes.

= 0.2.1 =
WP.org compliance fixes. Token salt storage changes from wp-config.php to wp_options — existing tokens remain readable. Safe to update in place.

= 0.2.0 =
Adds Twitter/X, YouTube, and TikTok source adapters. No database changes — safe to update in place.

= 0.1.0 =
Initial release — no upgrade path required.
