From 2fe77faf0f3d4d7286b2dfa37843407c1df7060e Mon Sep 17 00:00:00 2001 From: Lawrence Hook Date: Sun, 1 Mar 2026 21:57:22 -0500 Subject: [PATCH] version 4.3.72 Squashed commit of the following: commit 53a18e7987ec332b280be0fe473336df332f8c6a Author: Lawrence Hook Date: Sun Mar 1 16:56:50 2026 -0500 Hide donate link for premium users instead of showing dead label Co-Authored-By: Claude Opus 4.6 commit e2732dee6fbe2aeac77a56e87281b01000a13d89 Author: Lawrence Hook Date: Sun Mar 1 16:50:33 2026 -0500 Fix fast-xml-parser vulnerability via npm override and update @aws-sdk/client-ses Co-Authored-By: Claude Opus 4.6 commit 281c0f110360f5fb4109a527306bf55ab2f5fc5c Author: Lawrence Hook Date: Sun Mar 1 16:46:31 2026 -0500 Add "Active" sidebar tab and soften selected sidebar styling Adds "all" and "active" tabs at the top of the sidebar. "Active" shows all currently-enabled options in one place; "all" explicitly resets to the default view. Also lightens the sidebar selected state from solid black to a subtle tint. Co-Authored-By: Claude Opus 4.6 commit 56b119ed98cb37cad65ed2fce7a7e34a62e3060b Author: Lawrence Hook Date: Sun Feb 22 05:01:00 2026 -0500 Cache grandfathered emails in memory at startup Co-Authored-By: Claude Opus 4.6 commit afed16a38a87332fd512c16e4cac80f04da6fa26 Author: Lawrence Hook Date: Sun Feb 22 04:47:51 2026 -0500 Add tests for cache TTL and webhook ordering race condition Co-Authored-By: Claude Opus 4.6 commit 361a8caf847a9ef48406db1857307d38494a5432 Author: Lawrence Hook Date: Sun Feb 22 04:34:00 2026 -0500 Harden license cache: 10s TTL and skip incomplete subscription.created Co-Authored-By: Claude Opus 4.6 commit d857ae8c266b5f19b5349584bcfa6215adae0fa3 Author: Lawrence Hook Date: Sat Feb 21 18:24:33 2026 -0500 Update README: replace donations blurb with premium link Co-Authored-By: Claude Opus 4.6 commit c473314ca546092b88b5f2d613cc5d5b8f4e695d Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat Feb 21 18:22:39 2026 -0500 Bump fast-xml-parser and @aws-sdk/xml-builder in /server (#201) Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) and [@aws-sdk/xml-builder](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/xml-builder). These dependencies needed to be updated together. Updates `fast-xml-parser` from 5.3.4 to 5.3.6 - [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases) - [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.4...v5.3.6) Updates `@aws-sdk/xml-builder` from 3.972.4 to 3.972.5 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/xml-builder/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/xml-builder) --- updated-dependencies: - dependency-name: fast-xml-parser dependency-version: 5.3.6 dependency-type: indirect - dependency-name: "@aws-sdk/xml-builder" dependency-version: 3.972.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 49e1a5d9ba7022b9f69afe399edff9cf1ad92eca Author: Lawrence Hook Date: Sat Feb 21 17:30:29 2026 -0500 Fix license cache not updating after checkout The checkout.session.completed webhook only sent a welcome email but did not update the subscription cache, so users kept getting cached free status after paying. Co-Authored-By: Claude Opus 4.6 commit c5734518c407ce436d0b281ab61c18ca21e0f6bb Author: Lawrence Hook Date: Sun Feb 15 19:10:05 2026 -0500 Remove TESTING.md and DEPLOYMENT.md Co-Authored-By: Claude Opus 4.6 commit 6ed3ad02910ea4d39dc578b8e87de092b76508ff Author: Lawrence Hook Date: Sun Feb 15 19:08:03 2026 -0500 Remove obsolete docs and update TODO for YouTube DOM change Co-Authored-By: Claude Opus 4.6 commit 83693504e3b386f681ac0cb678180eb9d19c5acd Author: Lawrence Hook Date: Sun Feb 15 18:54:29 2026 -0500 update versions commit 27a6ae6f18f2b2942a79f293d87164ae7609f0b5 Merge: 3f4b42b d6e642b Author: Lawrence Hook Date: Sun Feb 15 18:53:37 2026 -0500 Merge branch 'released' commit 3f4b42b2dea44f957449816a79fdf5f4d2399312 Author: Lawrence Hook Date: Sun Feb 15 18:49:28 2026 -0500 Update qs to 6.14.2 to fix low-severity DoS vulnerability Co-Authored-By: Claude Opus 4.6 commit 8825666512ff6bfb16b237f3180af4ed259583a8 Author: Lawrence Hook Date: Sun Feb 15 18:45:37 2026 -0500 Merge the monetization feature branch. Add RYS Premium subscription system Introduce optional paid tier with sign-in, Stripe billing, and premium feature gating. Core features remain free. Includes Node.js server for auth, payments, and license management. Extension changes: - Sign-in flow with magic link email - Premium/upgrade/account modals with branded UI - Premium feature gating (60+ advanced settings) - Password lock and scheduling (premium) - Fix "hide all but first row" for YouTube's new flat homepage DOM - Hide sidebar ad panels by default - Enable "Hide all Shorts" by default Server: - Magic link auth with rate limiting - Stripe checkout, webhooks, and billing portal - JWT-based license tokens - AWS SES transactional emails (welcome, sign-in, cancellation) - Grandfathered donor support Also adds CI workflow, server test suite, and extension unit tests. commit a76f69804b46ad8cd579e3b325683b0428cdcf3a Author: Lawrence Hook Date: Sun Feb 8 20:45:35 2026 -0500 update description in README Co-Authored-By: Claude Opus 4.6 commit b5d1384ca88c6c16d190cf8d5a189be7b6db7c53 Author: Lawrence Hook Date: Sun Feb 8 20:44:26 2026 -0500 update "Why I made it" section in README Co-Authored-By: Claude Opus 4.6 commit 18b64708b689641aabe367781f9889552717a363 Author: Lawrence Hook Date: Sun Feb 8 20:14:23 2026 -0500 update version to 4.3.71 --- DEPLOYMENT.md | 521 ----------------- ISSUES.md | 8 - LAUNCH_CHECKLIST.md | 40 -- README.md | 2 +- TESTING.md | 357 ------------ TODO.md | 2 +- premium-subscription-spec.md | 682 ---------------------- server/package-lock.json | 1034 ++++++++++++++++------------------ server/package.json | 5 +- server/src/index.js | 1 + server/src/routes/webhook.js | 10 +- server/src/storage/index.js | 18 +- server/tests/license.test.js | 4 +- server/tests/storage.test.js | 37 ++ server/tests/webhook.test.js | 178 ++++++ src/chrome_manifest.json | 2 +- src/firefox_manifest.json | 2 +- src/options/body.css | 43 +- src/options/main.js | 175 +++++- src/options/options.css | 4 + src/options/settings-menu.js | 8 +- 21 files changed, 945 insertions(+), 2188 deletions(-) delete mode 100644 DEPLOYMENT.md delete mode 100644 ISSUES.md delete mode 100644 LAUNCH_CHECKLIST.md delete mode 100644 TESTING.md delete mode 100644 premium-subscription-spec.md create mode 100644 server/tests/webhook.test.js diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index 755d519..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,521 +0,0 @@ -# RYS Premium Server Deployment Guide - -Complete guide to deploying the premium subscription server and testing the monetization flow. - -**Server URL:** `https://server.lawrencehook.com/rys/` - ---- - -## Table of Contents - -1. [Prerequisites](#prerequisites) -2. [Stripe Setup](#stripe-setup) -3. [AWS SES Setup](#aws-ses-setup) -4. [Server Deployment](#server-deployment) -5. [Apache Configuration](#apache-configuration) -6. [Extension Setup](#extension-setup) -7. [Testing](#testing) -8. [Troubleshooting](#troubleshooting) -9. [Maintenance](#maintenance) - ---- - -## Prerequisites - -- AWS Lightsail instance (Bitnami stack) -- Node.js v20+ -- Stripe account (test mode for development) -- AWS SES configured for sending emails - ---- - -## Stripe Setup - -### Create Products - -1. Go to https://dashboard.stripe.com/test/products -2. Click **"+ Add product"** - -**Monthly Plan:** -- Name: `RYS Premium Monthly` -- Pricing: `$3.00 USD` / `month` / `recurring` -- Save and copy the **Price ID** (starts with `price_...`) - -**Yearly Plan:** -- Name: `RYS Premium Yearly` -- Pricing: `$24.00 USD` / `year` / `recurring` -- Save and copy the **Price ID** - -### Get API Keys - -1. Go to https://dashboard.stripe.com/test/apikeys -2. Copy the **Secret key** (starts with `sk_test_...`) - -### Configure Billing Portal - -1. Go to Settings → Billing → Customer portal -2. Enable the customer portal -3. Configure allowed actions: - - [x] Update payment methods - - [x] View invoice history - - [x] Cancel subscriptions -4. Save changes - -### Create Webhook Endpoint - -1. Go to https://dashboard.stripe.com/test/webhooks -2. Click **"+ Add endpoint"** -3. Endpoint URL: `https://server.lawrencehook.com/rys/webhook/stripe` -4. Select events: - - `checkout.session.completed` - - `customer.subscription.created` - - `customer.subscription.updated` - - `customer.subscription.deleted` - - `invoice.payment_failed` - - `invoice.payment_succeeded` -5. Click **"Add endpoint"** -6. Copy the **Signing secret** (starts with `whsec_...`) - ---- - -## AWS SES Setup - -### Verify Sender Email/Domain - -1. In AWS Console → SES → Verified identities -2. Click "Create identity" -3. For email: click the verification link sent to you -4. For domain: add the DNS records provided - -### Request Production Access - -SES starts in sandbox mode (can only send to verified emails). - -1. Go to SES → Account dashboard -2. Click "Request production access" -3. Fill out the form explaining your use case -4. Wait for approval (usually 24-48 hours) - -### IAM Credentials - -If not using instance roles, create IAM credentials: - -1. Create a new user (e.g., `rys-premium-ses`) -2. Attach policy: `AmazonSESFullAccess` -3. Create access keys -4. Copy the **Access Key ID** and **Secret Access Key** - ---- - -## Server Deployment - -### SSH to Lightsail Instance - -```bash -ssh bitnami@your-instance-ip -``` - -### Install Node.js 20+ (if needed) - -```bash -curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - -sudo apt-get install -y nodejs -node --version # Should show v20.x.x -``` - -### Install PM2 - -```bash -sudo npm install -g pm2 -``` - -### Clone and Setup Repository - -```bash -cd ~/github -git clone https://github.com/lawrencehook/remove-youtube-suggestions.git -cd remove-youtube-suggestions -git checkout feature/monetize -cd server -npm install -``` - -### Create Environment File - -```bash -nano .env -``` - -Paste the following (fill in your values): - -```bash -# Server -PORT=3005 -BASE_URL=https://server.lawrencehook.com/rys - -# JWT (generate with: openssl rand -base64 32) -JWT_SECRET=your-random-secret-at-least-32-characters-long - -# Stripe -STRIPE_SECRET_KEY=sk_test_YOUR_KEY -STRIPE_PRICE_MONTHLY=price_YOUR_MONTHLY_ID -STRIPE_PRICE_YEARLY=price_YOUR_YEARLY_ID -STRIPE_WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET - -# AWS SES (uses instance role, no explicit credentials needed) -AWS_REGION=us-east-1 -EMAIL_FROM=noreply@lawrencehook.com -``` - -Save: `Ctrl+O`, `Enter`, `Ctrl+X` - -### Create Data Directory - -```bash -mkdir -p data -echo '[]' > data/grandfathered.json -``` - -To add grandfathered users (past donors with lifetime access): -```bash -echo '["donor1@example.com", "donor2@example.com"]' > data/grandfathered.json -``` - -### Test Server Starts - -```bash -npm start -``` - -Expected output: -``` -Loaded X grandfathered emails -RYS Premium Server running on port 3005 -Base URL: https://server.lawrencehook.com/rys -``` - -Press `Ctrl+C` to stop. - -### Start with PM2 - -```bash -pm2 start src/index.js --name rys-premium -pm2 save -``` - -### Configure PM2 Auto-Start on Reboot - -```bash -pm2 startup -# Run the command it outputs (starts with: sudo env PATH=...) -pm2 save -``` - ---- - -## Apache Configuration - -Your Lightsail instance uses Bitnami's Apache with path-based proxying. - -### Edit SSL Config - -Edit your local deployments repo: - -```bash -nano ~/github/deployments/opt/bitnami/apache/conf/bitnami/bitnami-ssl.conf -``` - -Add these lines inside the `` block: - -```apache -ProxyPass /rys/ http://localhost:3005/ -ProxyPassReverse /rys/ http://localhost:3005/ -``` - -### Deploy Apache Config - -```bash -# Local machine -cd ~/github/deployments -git add -A && git commit -m "Add RYS Premium server proxy config" && git push - -# On server -cd ~/github/deployments && git pull -./update_apache_conf.sh -``` - ---- - -## Extension Setup - -### Verify Config Points to Production - -Check `src/shared/config.js`: - -```javascript -const PREMIUM_CONFIG = { - SERVER_URL: 'https://server.lawrencehook.com/rys', - // ... -}; -``` - -### Build the Extension - -**For Chrome:** -```bash -./make_chrome.sh -``` - -**For Firefox:** -```bash -./make_firefox.sh -``` - -### Load in Chrome - -1. Navigate to `chrome://extensions/` -2. Enable **"Developer mode"** (toggle in top-right) -3. Click **"Load unpacked"** -4. Select: `chrome_extension/` - -### Load in Firefox - -1. Navigate to `about:debugging#/runtime/this-firefox` -2. Click **"Load Temporary Add-on..."** -3. Select: `src/firefox_manifest.json` - ---- - -## Testing - -### Automated Tests - -```bash -cd server -npm test -``` - -All tests should pass (currently 46). - -### Health Check - -```bash -curl https://server.lawrencehook.com/rys/health -# Expected: {"status":"ok","timestamp":"..."} -``` - -### Magic Link Flow - -**Via curl:** -```bash -curl -X POST https://server.lawrencehook.com/rys/auth/send-magic-link \ - -H "Content-Type: application/json" \ - -d '{"email":"your-email@example.com"}' -# Expected: {"request_id":"abc123-..."} -``` - -**Via extension:** -1. Go to any YouTube page -2. Click the RYS extension icon -3. Click the gear icon (settings menu) -4. Click **"Sign In"** -5. Enter your email and click **"Send Sign-In Link"** -6. Check your email and click the magic link -7. Return to extension — should show "Signed in successfully" - -### License Check - -After signing in: -1. Click gear icon → **"Account"** -2. Should show your email and status: **"Free"** - -**For grandfathered users:** -- Add email to `data/grandfathered.json` on server -- Restart: `pm2 restart rys-premium` -- Sign out and sign back in -- Status should show: **"Premium (Lifetime)"** - -### Checkout Flow - -1. Open Account modal -2. Click **"Upgrade to Premium"** -3. Select a plan (Monthly or Yearly) -4. Click **"Continue to Checkout"** -5. Complete payment with test card: - - Card: `4242 4242 4242 4242` - - Expiry: Any future date (e.g., `12/34`) - - CVC: Any 3 digits (e.g., `123`) - - ZIP: Any valid ZIP (e.g., `12345`) -6. After success page, return to extension -7. Open Account — status should show **"Premium Active"** - -### Billing Portal - -1. With an active subscription, open Account modal -2. Click **"Manage Subscription"** -3. Stripe billing portal opens in new tab -4. Can cancel subscription here for testing - -### Webhook Verification - -1. Go to Stripe Dashboard → Webhooks → your endpoint -2. Click **"Send test webhook"** -3. Select `checkout.session.completed` -4. Check server logs: `pm2 logs rys-premium` - -### Extension Smoke Tests - -- **Sign-in flow:** Send magic link → click link → see "Signed in successfully" -- **Poll cancel:** Start sign-in → hit Cancel → spinner stops, no sign-in after cancel -- **Premium gate:** Mark option as premium → non-premium click opens upgrade modal -- **License refresh:** Open Account → premium label reflects correct status -- **Upgrade flow:** Choose plan → click checkout → Stripe checkout opens -- **Billing portal:** With subscription → click "Manage Subscription" → portal opens - -### Pre-Launch Checklist - -- [ ] All automated tests pass -- [ ] Server starts without errors -- [ ] `/health` endpoint returns OK -- [ ] Magic link emails are received -- [ ] Magic link verification works -- [ ] Grandfathered users show as premium -- [ ] Stripe checkout creates a session -- [ ] Stripe webhook is configured and verified -- [ ] Billing portal is accessible -- [ ] SSL certificate is valid -- [ ] CORS works from extension origins - -### Test Cards Reference - -| Card | Result | -|------|--------| -| `4242 4242 4242 4242` | Success | -| `4000 0000 0000 0002` | Declined | -| `4000 0025 0000 3155` | Requires authentication | - ---- - -## Troubleshooting - -### Server won't start - -```bash -pm2 logs rys-premium --lines 50 -``` - -Common issues: -- Missing .env variables -- JWT_SECRET too short (needs 32+ chars) -- Port already in use - -### Magic link emails not sending - -- Check SES is configured -- Verify EMAIL_FROM domain is verified in SES -- If in SES sandbox, recipient must also be verified - -### CORS errors in extension - -```bash -# Verify server is running -curl http://localhost:3005/health -``` - -### SSL certificate issues - -```bash -sudo /opt/bitnami/bncert-tool # regenerate if needed -``` - -### Extension can't connect - -```bash -# Test endpoint -curl https://server.lawrencehook.com/rys/health - -# Check Apache config -sudo apachectl configtest -``` - ---- - -## Maintenance - -### Update Server Code - -```bash -cd ~/github/remove-youtube-suggestions -git pull -git checkout feature/monetize -cd server -npm install -pm2 restart rys-premium -``` - -### View Logs - -```bash -pm2 logs rys-premium # Tail logs -pm2 logs rys-premium --lines 100 # Last 100 lines -``` - -### Server Commands - -```bash -pm2 restart rys-premium # Restart -pm2 stop rys-premium # Stop -pm2 status # Check status -``` - -### Add to Deployment Script - -Add to `~/github/deployments/run`: - -```bash -# RYS Premium Server (pm2) -cd /home/bitnami/github/remove-youtube-suggestions && git checkout -- . && git pull && git checkout feature/monetize; -cd server && npm install && npm audit fix; -pm2 restart rys-premium || pm2 start src/index.js --name rys-premium; -``` - -### Updating Grandfathered List - -1. Edit `data/grandfathered.json` -2. Restart: `pm2 restart rys-premium` - ---- - -## Environment Variables Reference - -| Variable | Description | Example | -|----------|-------------|---------| -| `PORT` | Server port | `3005` | -| `BASE_URL` | Public URL | `https://server.lawrencehook.com/rys` | -| `JWT_SECRET` | 32+ char secret | (generate with `openssl rand -base64 32`) | -| `STRIPE_SECRET_KEY` | Stripe API secret | `sk_test_...` | -| `STRIPE_PRICE_MONTHLY` | Monthly plan price ID | `price_...` | -| `STRIPE_PRICE_YEARLY` | Yearly plan price ID | `price_...` | -| `STRIPE_WEBHOOK_SECRET` | Webhook signing secret | `whsec_...` | -| `AWS_REGION` | AWS region for SES | `us-east-1` | -| `EMAIL_FROM` | Sender email address | `noreply@lawrencehook.com` | - ---- - -## API Endpoints Reference - -| Endpoint | Method | Auth | Description | -|----------|--------|------|-------------| -| `/health` | GET | No | Health check | -| `/auth/send-magic-link` | POST | No | Send magic link email | -| `/auth/verify` | GET | No | Magic link target (HTML) | -| `/auth/poll` | GET | No | Poll for auth status | -| `/license/check` | GET | Yes | Check premium status | -| `/checkout/create` | POST | Yes | Create Stripe checkout | -| `/checkout/success` | GET | No | Success page (HTML) | -| `/checkout/cancel` | GET | No | Cancel page (HTML) | -| `/billing/portal` | POST | Yes | Create billing portal session | -| `/billing/return` | GET | No | Return page (HTML) | -| `/webhook/stripe` | POST | No* | Stripe webhook | - -*Authenticated via Stripe signature diff --git a/ISSUES.md b/ISSUES.md deleted file mode 100644 index 2f9d126..0000000 --- a/ISSUES.md +++ /dev/null @@ -1,8 +0,0 @@ -# Issues Found During Testing - -1. **Footer missing Premium link** — Footer shows "Donors" but no "Premium" link for signed-in users. Fix: add a "Premium" link visible when signed in. ✅ Fixed -2. **"Premium features coming soon" banner** — Remove entirely since premium is now deployed. ✅ Fixed -3. **Settings menu icon** — Consider changing from three dots to a gear icon. ✅ Fixed -4. **Browser action icon** — Update the toolbar icon with a premium indicator when user has premium. (Deferred.) -5. **Static site branding** — Add "Premium" branding to the premium page and checkout/billing pages on lawrencehook.com/rys. (Server pages done; static site deferred until launch.) -6. **Email sender name** — Change "noreply" to "rys" for magic link emails. ✅ Fixed diff --git a/LAUNCH_CHECKLIST.md b/LAUNCH_CHECKLIST.md deleted file mode 100644 index 8fa1e92..0000000 --- a/LAUNCH_CHECKLIST.md +++ /dev/null @@ -1,40 +0,0 @@ -# Premium Launch Checklist - -## Pre-Launch - -### Configuration -- [x] Update `grandfathered.txt` with all donor emails -- [x] Switch Stripe keys from test to production - - `STRIPE_SECRET_KEY` - - `STRIPE_PRICE_MONTHLY` - - `STRIPE_PRICE_YEARLY` - - `STRIPE_WEBHOOK_SECRET` -- [x] Configure production webhook endpoint in Stripe dashboard -- [x] Verify `EMAIL_FROM` is set correctly for production - -### Code -- [ ] Merge `feature/monetize` branch into `main` -- [x] Redeploy server with production config -- [ ] Test production sign-in flow (magic link email) -- [ ] Test production checkout flow (real Stripe) - -### Extension -- [ ] Build Chrome extension -- [ ] Build Firefox extension -- [ ] Submit to Chrome Web Store -- [ ] Submit to Firefox Add-ons - -### Static Site (lawrencehook.com/rys) -- [ ] Update `/rys/premium/index.html` — change "Premium is Coming" to live copy -- [ ] Add sign-in/upgrade CTA to premium page - -## Post-Launch - -- [ ] Verify webhooks are received in production -- [ ] Monitor for errors in server logs -- [ ] Test full flow as a new user - -## Optional / Deferred - -- [ ] Browser action icon — premium indicator in toolbar -- [x] Add timestamps to server logs diff --git a/README.md b/README.md index 7b71d24..2a396c8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Leave a review! - [Chrome](https://chrome.google.com/webstore/detail/remove-youtube-suggestion/cdhdichomdnlaadbndgmagohccgpejae) - [Google Form](https://docs.google.com/forms/d/1AzQQxTWgG6M5N87jinvXKQkGS6Mehzg19XV4mjteTK0/edit) -Free to use with optional premium features. Donations welcome — [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=FF9K9YD6K6SWG¤cy_code=USD&source=url) +Free to use with optional [premium features](https://lawrencehook.com/rys/premium/). --- diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 6fb18f3..0000000 --- a/TESTING.md +++ /dev/null @@ -1,357 +0,0 @@ -# Manual Testing Guide — Premium Subscription - -This document covers all manual testing paths for the premium monetization feature. - -## Prerequisites - -- Extension loaded unpacked in Chrome (or Firefox) -- Server running locally or deployed to `server.lawrencehook.com` -- Stripe test mode configured with test price IDs -- AWS SES configured (or use a local email trap) -- At least one email in `data/grandfathered.json` for grandfathered testing -- A separate non-grandfathered email for regular user testing -- Stripe CLI installed for webhook testing (`stripe listen --forward-to localhost:3000/rys/webhook/stripe`) - ---- - -## 1. Initial State (Not Signed In) - -### 1.1 Options page loads correctly -- [ ] Open extension popup — settings menu shows "Sign In" (not "Account") -- [ ] "Donate" link visible in header, links to PayPal - -### 1.2 Free features work without sign-in -- [ ] Toggle a non-premium feature (e.g., "Hide homepage recommendations") -- [ ] Verify it takes effect on YouTube - -### 1.3 Premium feature click — not signed in -- [ ] Click any premium-gated toggle (marked with a lock/premium indicator) -- [ ] Verify the "Premium Required" modal appears -- [ ] Click "Cancel" — modal closes, setting unchanged -- [ ] Click the premium toggle again, then click "Sign In" in the modal -- [ ] Verify a new tab opens to `main.html?signin=1` and the popup closes - -### 1.4 Schedule and Password (premium settings menu items) -- [ ] Click "Schedule" in the settings menu — Premium Required modal appears -- [ ] Click "Password" in the settings menu — Premium Required modal appears - ---- - -## 2. Sign-In Flow - -### 2.1 Happy path — sign in from new tab -- [ ] From the options tab (`main.html?signin=1`), sign-in modal opens automatically -- [ ] Enter a valid email, press Enter (or click send button) -- [ ] Modal transitions to waiting state with countdown timer -- [ ] Check email — magic link email received from the configured sender -- [ ] Click magic link — browser opens server success page ("You're signed in!") -- [ ] Return to extension tab — modal closes, status shows "Signed in successfully" -- [ ] Header now shows "Account" instead of "Sign In" - -### 2.2 Sign in from popup -- [ ] Open extension popup, click "Sign In" -- [ ] Verify popup closes and a new tab opens to `main.html?signin=1` -- [ ] Complete the sign-in flow in the new tab - -### 2.3 Invalid email -- [ ] Enter an invalid email (no @ symbol) -- [ ] Verify "Please enter a valid email" status message appears -- [ ] No network request is made - -### 2.4 Rate limiting -- [ ] Send 5 magic link requests for the same email in rapid succession -- [ ] On the 6th attempt, verify a rate limit error message appears -- [ ] Wait for the rate limit window to pass (or clear rate limit data on server) - -### 2.5 Cancel during polling -- [ ] Start sign-in flow, reach waiting state -- [ ] Click "Cancel" button -- [ ] Verify modal closes and polling stops (no continued network requests) -- [ ] Verify user is still not signed in - -### 2.6 Poll timeout -- [ ] Start sign-in flow but do NOT click the magic link -- [ ] Wait for the 16-minute countdown to reach 0 -- [ ] Verify error state appears with "Verification timed out" message -- [ ] Click "Retry" — email form reappears - -### 2.7 Expired magic link -- [ ] Start sign-in flow, wait >15 minutes, then click the magic link -- [ ] Verify the server shows an error page (link expired) -- [ ] Extension should show error state when poll detects 404 - -### 2.8 Multiple sign-in attempts -- [ ] Start sign-in, reach waiting state -- [ ] Close modal, reopen, start a new sign-in with a different email -- [ ] Verify the previous polling is aborted (no interference) - ---- - -## 3. Signed-In, Free User - -### 3.1 Account modal -- [ ] Click "Account" in settings menu -- [ ] Verify Account modal opens showing: - - Your email address - - "Free Plan" status - - "Upgrade" button visible - - "Billing" button hidden - - "Sign Out" button visible - -### 3.2 Premium feature click — signed in, not premium -- [ ] Click a premium feature toggle -- [ ] Verify the Upgrade modal appears (NOT the Premium Required modal) - -### 3.3 Upgrade modal -- [ ] Verify Upgrade modal shows plan selection (Monthly / Yearly) -- [ ] Verify Yearly is selected by default -- [ ] Click Monthly — verify it becomes selected -- [ ] Click Yearly — verify it becomes selected -- [ ] Click "Cancel" — modal closes - -### 3.4 License check — no subscription -- [ ] Open DevTools, check console for `[license] email -> free` on server -- [ ] Verify `is_premium` attribute on `` is `"false"` - ---- - -## 4. Checkout / Purchase Flow - -### 4.1 Monthly subscription -- [ ] Click a premium feature → Upgrade modal -- [ ] Select "Monthly", click "Subscribe" -- [ ] Verify Stripe Checkout opens in a new tab -- [ ] Complete payment with Stripe test card (`4242 4242 4242 4242`) -- [ ] Verify Stripe success page appears -- [ ] Return to extension tab (click on it or close Stripe tab) -- [ ] Verify extension auto-refreshes license (visibility/focus event triggers refresh) -- [ ] Verify the premium feature you clicked is now enabled -- [ ] Verify "Donate" link changed to "Premium" in header - -### 4.2 Yearly subscription -- [ ] Sign out, sign in with a different email -- [ ] Repeat checkout flow selecting "Yearly" -- [ ] Verify Stripe Checkout shows the yearly price -- [ ] Complete payment and verify premium activates - -### 4.3 Cancel checkout -- [ ] Open Upgrade modal, click "Subscribe" -- [ ] On Stripe Checkout page, click back or close the tab -- [ ] Return to extension — verify user is still on Free Plan -- [ ] Verify no error messages appear - -### 4.4 Checkout with expired session -- [ ] Clear `session_token` from `chrome.storage.local` manually (DevTools → Application → Local Storage) -- [ ] Try to subscribe — verify error about session expired -- [ ] Verify user is signed out and prompted to sign in again - ---- - -## 5. Premium User Experience - -### 5.1 All premium features accessible -- [ ] Toggle several premium features on — verify they take effect on YouTube -- [ ] Toggle them off — verify they revert - -### 5.2 Account modal — premium subscriber -- [ ] Open Account modal -- [ ] Verify it shows: - - Your email - - "Premium Active" status - - "Billing" button visible - - "Upgrade" button hidden - - "Sign Out" button - -### 5.3 Billing portal -- [ ] Click "Billing" button in Account modal -- [ ] Verify Stripe billing portal opens in new tab -- [ ] Verify you can see subscription details, update payment method, cancel -- [ ] Close billing portal tab, return to extension - -### 5.4 Schedule (premium feature) -- [ ] Click "Schedule" in settings menu -- [ ] Verify Schedule modal opens (not blocked by premium gate) -- [ ] Configure a schedule and verify it works - -### 5.5 Password (premium feature) -- [ ] Click "Password" in settings menu -- [ ] Verify Password modal opens (not blocked by premium gate) -- [ ] Set a password, close settings, reopen — verify password prompt appears -- [ ] Enter correct password — verify unlock works -- [ ] Disable password — verify it's removed - ---- - -## 6. Grandfathered User - -### 6.1 Sign in with grandfathered email -- [ ] Sign in with an email that's in `data/grandfathered.json` -- [ ] Verify license check returns `grandfathered: true` - -### 6.2 Account modal — grandfathered -- [ ] Open Account modal -- [ ] Verify it shows: - - Your email - - "Lifetime Premium" status - - "Billing" button hidden (no subscription to manage) - - "Upgrade" button hidden - - "Sign Out" button - -### 6.3 Premium features work -- [ ] Verify all premium features are accessible -- [ ] Verify "Donate" link shows "Premium" - -### 6.4 Case-insensitive matching -- [ ] Add `test@example.com` to `grandfathered.json` -- [ ] Sign in with `Test@Example.com` (different case) -- [ ] Verify user is recognized as grandfathered - ---- - -## 7. Sign Out - -### 7.1 Sign out clears state -- [ ] While signed in (premium or free), click "Sign Out" in Account modal -- [ ] Verify header changes back to "Sign In" -- [ ] Verify "Donate" link reappears (if was showing "Premium") -- [ ] Verify premium features are re-gated - -### 7.2 Storage is cleared -- [ ] After sign-out, check `chrome.storage.local` in DevTools -- [ ] Verify `session_token`, `license_token`, and `user_email` are removed -- [ ] Verify YouTube settings (non-premium) are NOT affected - ---- - -## 8. Session Expiry - -### 8.1 Expired session token -- [ ] Sign in, then manually modify the `session_token` in storage to an expired JWT -- [ ] Open Account modal (or trigger a license check) -- [ ] Verify "Session expired. Please sign in again." message appears -- [ ] Verify user is automatically signed out -- [ ] Verify sign-in modal opens - -### 8.2 Invalid session token -- [ ] Replace `session_token` with garbage string in storage -- [ ] Trigger a license check -- [ ] Verify 401 from server causes automatic sign-out - ---- - -## 9. License Token Caching & Refresh - -### 9.1 Cached token is used -- [ ] Sign in as premium user, verify license check succeeds -- [ ] Disconnect from network (or stop the server) -- [ ] Close and reopen extension popup -- [ ] Verify premium features are still accessible (cached license token is valid) - -### 9.2 Expired cached token without network -- [ ] Manually set `license_token` to an expired JWT in storage -- [ ] With server unreachable, open extension -- [ ] Verify user shows as non-premium (expired cache, can't refresh) - -### 9.3 Token refresh near expiry -- [ ] Set a license token that expires within 24 hours (modify JWT payload) -- [ ] Open extension with server running -- [ ] Verify a fresh license check is triggered (not using cache) - -### 9.4 Force refresh after checkout -- [ ] Complete a checkout flow -- [ ] Verify the extension calls `checkLicense(true)` (force refresh) -- [ ] Monitor network tab — a `/license/check` request should fire immediately on tab focus - ---- - -## 10. Subscription Lifecycle - -### 10.1 Cancel subscription via billing portal -- [ ] As a premium user, open Billing portal -- [ ] Cancel the subscription in Stripe -- [ ] Return to extension, close and reopen popup -- [ ] Wait for license token to expire (or force refresh) -- [ ] Verify user shows as "Free Plan" after refresh -- [ ] Verify premium features are re-gated - -### 10.2 Resubscribe after cancellation -- [ ] After cancellation, click a premium feature -- [ ] Go through checkout flow again -- [ ] Verify premium re-activates - ---- - -## 11. Stripe Webhooks - -### 11.1 Webhook receives events -- [ ] With Stripe CLI forwarding, complete a checkout -- [ ] Verify `checkout.session.completed` event is logged on server -- [ ] Cancel subscription — verify `customer.subscription.deleted` is logged - -### 11.2 Invalid webhook signature -- [ ] Send a POST to `/webhook/stripe` with invalid/missing `Stripe-Signature` header -- [ ] Verify 400 response - ---- - -## 12. Cross-Browser - -### 12.1 Chrome -- [ ] Run through sign-in, checkout, and premium feature flows on Chrome - -### 12.2 Firefox -- [ ] Load extension in Firefox -- [ ] Verify sign-in flow works (popup-to-tab handoff, polling) -- [ ] Verify premium features gate correctly -- [ ] Verify `browser.storage.local` API works as expected - ---- - -## 13. Edge Cases - -### 13.1 Multiple tabs -- [ ] Open extension settings in two tabs -- [ ] Sign in on one tab -- [ ] Refresh the second tab — verify it reflects signed-in state - -### 13.2 Rapid feature toggle clicks -- [ ] Click a premium feature rapidly multiple times -- [ ] Verify only one modal opens, no duplicate modals or errors - -### 13.3 Network failure during sign-in -- [ ] Start sign-in, disconnect network after magic link is sent -- [ ] Verify polling handles network errors gracefully (retries silently) -- [ ] Reconnect network — verify polling resumes and succeeds when link is clicked - -### 13.4 Server down during license check -- [ ] Sign in successfully, then stop the server -- [ ] Trigger a license check (reopen popup) -- [ ] Verify cached license token is used if still valid -- [ ] Verify no error shown to user - -### 13.5 Extension update while signed in -- [ ] Sign in and get premium status -- [ ] Reload the extension (simulate update) -- [ ] Verify session and license tokens persist across reload -- [ ] Verify premium status is maintained - ---- - -## 14. Analytics Verification - -Use Mixpanel (or check console logs) to verify events fire correctly: - -- [ ] `Sign In Started` — when email is submitted -- [ ] `Magic Link Sent` — after server responds -- [ ] `Sign In Success` — after verification completes -- [ ] `Sign In Canceled` — when user cancels polling -- [ ] `Sign In Error` — on timeout or failure -- [ ] `Premium Feature Click` — with correct `signedIn` flag -- [ ] `Upgrade Modal Opened` — when upgrade modal shows -- [ ] `Checkout Started` — with correct `plan` value -- [ ] `Checkout Completed` — after returning from successful checkout -- [ ] `Account Modal Opened` — with `isPremium` and `source` -- [ ] `Billing Portal Opened` — when billing portal opens -- [ ] `Session Expired` — when 401 triggers sign-out -- [ ] `Sign Out` — when user signs out -- [ ] `License Check` — with `cached`, `offline`, `isPremium` properties diff --git a/TODO.md b/TODO.md index 5c3432a..c740cf2 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,7 @@ Hardcoded English strings that break for non-English YouTube: ## Fragility YouTube frequently changes their DOM structure. Monitor these selectors: -- `ytd-rich-grid-row` +- `ytd-rich-item-renderer` (homepage now uses flat list instead of `ytd-rich-grid-row`) - `#metadata-line span` - `ytd-thumbnail-overlay-time-status-renderer` - `yt-chip-cloud-chip-renderer` diff --git a/premium-subscription-spec.md b/premium-subscription-spec.md deleted file mode 100644 index 83075a4..0000000 --- a/premium-subscription-spec.md +++ /dev/null @@ -1,682 +0,0 @@ -# Premium Subscription System Spec - -This document specifies the implementation of a premium subscription system for a browser extension. It is intended to be used as a reference for implementation. - -## Overview - -The system enables monetization of a free, open-source browser extension through a subscription model. Users authenticate via email magic links, purchase subscriptions through Stripe, and the extension gates premium features based on subscription status. - -### Key Design Decisions - -- **Single public repo**: Premium feature code is not hidden. Access is gated by license checks. -- **No application database**: Stripe is the source of truth for subscription status. -- **Magic link authentication**: Users verify email ownership by clicking a link sent to their inbox. -- **Polling-based handoff**: Extension polls the server to detect when magic link has been clicked. -- **Bypass tolerance**: Determined users circumventing the paywall is acceptable. - ---- - -## Configuration Constants - -All timing values should be defined in a central config file for easy adjustment. - -| Constant | Default Value | Description | -|----------|---------------|-------------| -| `MAGIC_LINK_EXPIRY` | 15 minutes | How long a magic link remains valid | -| `REQUEST_ID_EXPIRY` | 20 minutes | How long the server holds a pending auth request | -| `POLL_INTERVAL` | 2 seconds | How often extension polls during auth | -| `POLL_TIMEOUT` | 16 minutes | How long extension polls before giving up | -| `SESSION_TOKEN_LIFETIME` | 30 days | How long a session token remains valid | -| `LICENSE_TOKEN_LIFETIME` | 3 days | How long a license token remains valid | -| `GRANDFATHERED_TOKEN_LIFETIME` | 730 days | How long a grandfathered license token remains valid | -| `LICENSE_REFRESH_THRESHOLD` | 24 hours | Refresh license token if expiring within this window | -| `RATE_LIMIT_WINDOW` | 1 hour | Rate limit window for magic link requests | -| `RATE_LIMIT_MAX_REQUESTS` | 5 | Max magic link requests per email per window | - ---- - -## Data Models - -### AuthRequest (server-side, in-memory or temporary storage) - -Temporary record created when user initiates login. Can be stored in Redis, or in-memory if single-server. - -``` -{ - request_id: string, // Random, unguessable ID (e.g., UUID v4) - email: string, // User's email address - status: "pending" | "verified", - created_at: timestamp, - session_token: string | null // Populated once verified -} -``` - -### Session Token (JWT) - -Issued after successful magic link verification. Contains: - -``` -{ - email: string, // Verified email address - iat: number, // Issued at timestamp - exp: number // Expiration timestamp -} -``` - -Signed with a server-side secret (`JWT_SECRET` environment variable). - -### License Token (JWT) - -Issued by the server during license checks. Embedded premium status avoids extra network calls. Contains: - -``` -{ - email: string, // User's email address - premium: boolean, // Whether user has premium access - grandfathered: boolean, // Whether user is a past donor - exp: number // Expiration timestamp -} -``` - -Lifetime is 3 days for regular users, 730 days for grandfathered users. The extension refreshes the token when it's within 24 hours of expiry. - -### Extension Local Storage - -Stored via `chrome.storage.local` (works in both Chrome and Firefox): - -``` -{ - session_token: string | null, // JWT from auth flow - license_token: string | null, // JWT with embedded premium status - user_email: string | null // User's email for display -} -``` - ---- - -## Pricing - -| Plan | Price | Stripe Price ID | -|------|-------|-----------------| -| Monthly | $X.XX/month | Set in environment variable `STRIPE_PRICE_MONTHLY` | -| Yearly | $X.XX/year | Set in environment variable `STRIPE_PRICE_YEARLY` | - -Yearly plan should be positioned as a discount (e.g., ~2 months free). - ---- - -## Server Specification - -### Environment Variables - -``` -JWT_SECRET= -STRIPE_SECRET_KEY= -STRIPE_PRICE_MONTHLY= -STRIPE_PRICE_YEARLY= -STRIPE_WEBHOOK_SECRET= -EMAIL_FROM= -BASE_URL= -AWS_REGION= # defaults to us-east-1 -DATA_DIR= # defaults to ./data -``` - -Email is sent via AWS SES. AWS credentials are expected via standard environment or IAM role. - -### Endpoints - ---- - -#### `POST /auth/send-magic-link` - -Initiates the authentication flow. - -**Request body:** -```json -{ - "email": "user@example.com" -} -``` - -**Server behavior:** -1. Validate email format -2. Generate a random `request_id` (UUID v4) -3. Store AuthRequest with status "pending" -4. Send email containing link: `{BASE_URL}/auth/verify?token={request_id}` -5. Return `request_id` to caller - -**Response:** -```json -{ - "request_id": "abc123..." -} -``` - -**Errors:** -- `400` — Invalid email format -- `429` — Rate limited (5 requests per email per hour). Response includes `Retry-After` header. - ---- - -#### `GET /auth/verify` - -Magic link target. User's browser hits this when they click the email link. - -**Query parameters:** -- `token` — The `request_id` - -**Server behavior:** -1. Look up AuthRequest by `request_id` -2. If not found or expired → show error page -3. If found and pending: - - Generate JWT session token with user's email - - Update AuthRequest: status = "verified", session_token = JWT -4. Show success page: "You're signed in! You can close this tab." - -**Response:** HTML page (not JSON) - ---- - -#### `GET /auth/poll` - -Extension polls this to check if magic link has been clicked. - -**Query parameters:** -- `request_id` — The `request_id` returned from `/auth/send-magic-link` - -**Server behavior:** -1. Look up AuthRequest by `request_id` -2. If not found or expired → return error -3. If status is "pending" → return pending status -4. If status is "verified" → return session token, then delete the AuthRequest - -**Response (pending):** -```json -{ - "status": "pending" -} -``` - -**Response (verified):** -```json -{ - "status": "verified", - "session_token": "eyJhbGc...", - "email": "user@example.com" -} -``` - -**Errors:** -- `404` — Unknown or expired `request_id` - ---- - -#### `GET /license/check` - -Returns a signed license token containing the user's premium status. - -**Headers:** -``` -Authorization: Bearer -``` - -**Server behavior:** -1. Validate and decode JWT session token -2. Extract email from token -3. Check if email is in the grandfathered list (case-insensitive) -4. If grandfathered → return license token with `premium: true, grandfathered: true` (730-day lifetime) -5. Otherwise, query Stripe for customer by email -6. Check for active subscription -7. Return license token with `premium: true/false, grandfathered: false` (3-day lifetime) - -**Response:** -```json -{ - "license_token": "eyJhbGc..." -} -``` - -The license token is a JWT containing `{ email, premium, grandfathered, exp }`. - -**Errors:** -- `401` — Missing, invalid, or expired session token - ---- - -#### `POST /checkout/create` - -Creates a Stripe Checkout session for purchasing a subscription. - -**Headers:** -``` -Authorization: Bearer -``` - -**Request body:** -```json -{ - "plan": "monthly" | "yearly" -} -``` - -**Server behavior:** -1. Validate and decode JWT -2. Extract email from token -3. Look up or create Stripe customer by email -4. Create Stripe Checkout session with: - - Customer ID - - Appropriate price ID based on plan - - Success URL: `{BASE_URL}/checkout/success` - - Cancel URL: `{BASE_URL}/checkout/cancel` -5. Return checkout URL - -**Response:** -```json -{ - "checkout_url": "https://checkout.stripe.com/..." -} -``` - -**Errors:** -- `401` — Missing, invalid, or expired token -- `400` — Invalid plan value - ---- - -#### `POST /webhook/stripe` - -Receives Stripe webhook events. - -**Headers:** -- `Stripe-Signature` — Used to verify webhook authenticity - -**Server behavior:** -1. Verify webhook signature using `STRIPE_WEBHOOK_SECRET` -2. Handle relevant events: - - `checkout.session.completed` — Subscription created (optional: send welcome email) - - `customer.subscription.deleted` — Subscription canceled - - `customer.subscription.updated` — Subscription changed - - `invoice.payment_failed` — Payment failed (optional: send warning email) - -Since Stripe is the source of truth, these events are mainly useful for sending transactional emails or logging. The `/license/check` endpoint always queries Stripe directly. - -**Response:** -```json -{ - "received": true -} -``` - ---- - -#### `GET /checkout/success` - -Simple HTML page shown after successful checkout. - -**Content:** "Payment successful! You can close this tab and return to the extension." - ---- - -#### `GET /checkout/cancel` - -Simple HTML page shown if user cancels checkout. - -**Content:** "Payment canceled. You can close this tab and try again from the extension." - ---- - -#### `POST /billing/portal` - -Creates a Stripe billing portal session for managing an existing subscription. - -**Headers:** -``` -Authorization: Bearer -``` - -**Server behavior:** -1. Validate and decode JWT -2. Look up Stripe customer by email -3. Create billing portal session -4. Return portal URL - -**Response:** -```json -{ - "url": "https://billing.stripe.com/..." -} -``` - -**Errors:** -- `401` — Missing, invalid, or expired token -- `404` — No Stripe customer found for this email - ---- - -#### `GET /billing/return` - -Simple HTML page shown after returning from the Stripe billing portal. - -**Content:** "Billing updated. You can close this tab and return to the extension." - ---- - -### Stripe API Usage - -**Find customer by email:** -``` -stripe.customers.list({ email: email, limit: 1 }) -``` - -**Check for active subscription:** -``` -stripe.subscriptions.list({ customer: customer_id, status: 'active', limit: 1 }) -``` - -**Create customer:** -``` -stripe.customers.create({ email: email }) -``` - -**Create checkout session:** -``` -stripe.checkout.sessions.create({ - customer: customer_id, - mode: 'subscription', - line_items: [{ price: price_id, quantity: 1 }], - success_url: success_url, - cancel_url: cancel_url -}) -``` - ---- - -## Extension Specification - -### Permissions Required - -**manifest.json (Manifest V3 for Chrome, V2/V3 for Firefox):** - -```json -{ - "permissions": [ - "storage" - ], - "host_permissions": [ - "*://*.youtube.com/*" - ] -} -``` - -No special permissions beyond storage and YouTube host access. The premium server is accessed via `fetch()` which does not require additional host permissions in MV3. - ---- - -### Auth Module - -Handles sign-in and session management. - -#### `sendMagicLink(email: string): Promise` - -1. Call `POST /auth/send-magic-link` with email -2. Handle 429 (rate limit) with user-friendly error -3. Return `request_id` - -#### `pollForVerification(requestId, onStatusUpdate, options): Promise` - -1. Call `GET /auth/poll?request_id={requestId}` every 2 seconds -2. If status is "pending", call `onStatusUpdate` with elapsed time, continue polling -3. If status is "verified", store session token and email in `chrome.storage.local`, return `{ success: true }` -4. Timeout after 16 minutes with error -5. Supports `AbortController` signal via `options.signal` for cancellation -6. If aborted, return `{ canceled: true }` -7. Network errors during polling are silently retried; fatal errors (404, non-OK) throw - -#### `isSignedIn(): Promise` - -Returns true if a session token exists in storage. - -#### `getUserEmail(): Promise` - -Returns the stored user email. - -#### `getSessionToken(): Promise` - -Returns the stored session token. - -#### `signOut(): Promise` - -Clears session token, license token, and email from `chrome.storage.local`. - ---- - -### License Module - -Handles premium status checking and caching via license tokens (JWTs with embedded premium status). - -#### `checkLicense(forceRefresh?: boolean): Promise` - -1. Read cached session token and license token from `chrome.storage.local` -2. If no session token, return `{ isPremium: false }` -3. Decode license token JWT (no signature verification — we trust our server) -4. If not forcing refresh, token is valid, and not expiring within 24 hours: - - Return cached status from token payload -5. Otherwise, call `GET /license/check` with session token -6. If 401 response: auto sign-out, return `{ isPremium: false, signedOut: true }` -7. Store new license token in `chrome.storage.local` -8. Return status from new token -9. On network error: fall back to cached token if still valid (not expired) - -Returns `{ isPremium, source, cached?, offline?, signedOut?, error? }` where `source` is `'grandfathered'` or `null`. - -#### `isPremium(): Promise` - -Quick synchronous-style check. Reads cached license token, decodes JWT, returns `true` only if `premium === true` and token not expired. No network call. - -#### `createCheckoutSession(plan): Promise` - -Calls `POST /checkout/create` with plan, returns checkout URL. Auto signs out on 401. - -#### `createBillingPortalSession(): Promise` - -Calls `POST /billing/portal`, returns portal URL. Auto signs out on 401. - ---- - -### Checkout Module - -Handles upgrade flow. - -#### `startCheckout(plan: 'monthly' | 'yearly'): Promise` - -1. Get session token from auth module -2. If not signed in, throw error (UI should prompt sign-in first) -3. Call `POST /checkout/create` with plan -4. Open returned `checkout_url` in new tab - ---- - -### Feature Gating - -Features are marked with `premium: true` in `src/shared/main.js`. When a user clicks a premium-gated option: - -1. Check `HTML.getAttribute('is_premium')` -2. If not premium: - - Check if signed in via `Auth.isSignedIn()` - - If not signed in → show Premium Required Modal (then sign-in flow) - - If signed in but not premium → show Upgrade Modal -3. The `is_premium` attribute is set on page load via `License.checkLicense()` and updated after sign-in, checkout, and tab focus events - -Schedule and Password settings menus are also gated with the same `handlePremiumFeatureClick()` handler. - ---- - -### UI Components - -1. **Premium Required Modal** (for non-signed-in users) - - Shown when a non-signed-in user clicks a premium feature - - "Sign In" button → opens sign-in flow - - "Cancel" button → closes modal - -2. **Sign-In Modal** - - Email input field with Enter key support - - "Send magic link" button - - "Check your email" waiting state with countdown timer (16 min) - - Cancel button to abort polling (uses AbortController) - - Error state with retry button - - **Important**: When triggered from popup, opens a new tab (`main.html?signin=1`) because popup closing would kill the polling loop - -3. **Account Modal** - - Shows signed-in email - - Premium status display: - - "Lifetime Premium" (for grandfathered users, no billing button) - - "Premium Active" (for subscribers, with billing button) - - "Free Plan" (with upgrade button) - - Billing button → opens Stripe customer portal in new tab - - Sign-out button → clears all auth data - -4. **Upgrade Modal** - - Plan selection: Monthly / Yearly (defaults to yearly) - - "Subscribe" button → creates Stripe checkout session, opens in new tab - - Sets `awaitingUpgrade` flag; on tab refocus, auto-refreshes license - -5. **Header UI changes** - - "Sign In" text when not signed in → "Account" when signed in - - "Donate" link → changes to "Premium" (no link) when premium is active - ---- - -## Sequence Diagrams - -### Authentication Flow - -``` -User Extension Server Email - | | | | - |--Enter email---->| | | - | |--POST /send-magic-->| | - | |<--{ request_id }----| | - | | |----Magic link----->| - | |--GET /poll--------->| | - | |<--{ pending }-------| | - | | ... | | - |<-----------------+---------------------+-<--Click link------| - | | | | - | | GET /verify | - | | |--Show success page | - | |--GET /poll--------->| | - | |<--{ verified, token } | - | | | | - | |--Store token locally| | - |<--Signed in!-----| | | -``` - -### License Check Flow - -``` -Extension Server Stripe - | | | - |---GET /license/check----->| | - | (with session token) | | - | |---Get customer by email-->| - | |<--Customer data-----------| - | | | - | |---List subscriptions----->| - | |<--Subscription data-------| - | | | - |<--{ premium: true/false }--| | - | | | - |---Cache result locally | | -``` - -### Purchase Flow - -``` -User Extension Server Stripe - | | | | - |--Click upgrade-->| | | - | |--POST /checkout---->| | - | | { plan: "yearly" }| | - | | |--Create session--->| - | | |<--Session URL------| - | |<--{ checkout_url }--| | - | | | | - |<--Open checkout--| | | - | | | | - |------------ User completes payment on Stripe ------------->| - | | | | - | | |<--Webhook: paid----| - | | | | - |--Return to ext-->| | | - | |---Check license---->| | - | |<--{ premium: true }-| | - |<--Premium active!| | | -``` - ---- - -## Security Considerations - -1. **JWT secret**: Use a strong, random secret (>32 characters). Rotate periodically. -2. **HTTPS only**: All server endpoints must be HTTPS. -3. **Webhook verification**: Always verify Stripe webhook signatures. -4. **Rate limiting**: 5 requests per email per hour on `/auth/send-magic-link`. Returns 429 with `Retry-After` header. -5. **Request ID entropy**: UUIDv4 (128 bits of entropy). -6. **Token in memory during polling**: `request_id` kept in memory only, not persisted to storage. -7. **CORS**: Only chrome-extension:// and moz-extension:// origins allowed. -8. **License token decoding**: Extension decodes JWT payload without signature verification (trusts the server). This is acceptable given the bypass-tolerant design. - ---- - -## Error Handling - -### Extension-side - -- **Network errors**: Show retry option, fall back to cached license status -- **Auth expired**: Clear session, prompt re-login -- **Poll timeout**: Show "Link expired, try again" message - -### Server-side - -- **Invalid email**: 400 error with message -- **Expired magic link**: Show friendly error page with "Request new link" option -- **Stripe API errors**: Log error, return 500 with generic message -- **Invalid JWT**: 401 error - ---- - -## Testing - -See `TESTING.md` for detailed manual testing instructions covering all user paths. - ---- - -## Grandfathered Users - -Past donors are stored in `data/grandfathered.json` (array of email strings). During license checks: - -1. Email is matched case-insensitively -2. Grandfathered users receive a license token with `premium: true, grandfathered: true` and a 730-day lifetime -3. The UI displays "Lifetime Premium" and hides the billing portal button -4. No Stripe query is made for grandfathered users - -## Analytics - -Events are tracked via Mixpanel. Key events: - -- `License Check` — with `isPremium`, `source`, `cached`, `offline`, `error` properties -- `Premium Feature Click` — with `signedIn` flag -- `Sign In Started`, `Magic Link Sent`, `Sign In Success`, `Sign In Error`, `Sign In Canceled` -- `Checkout Started`, `Checkout Completed`, `Checkout Error` — with `plan` and `source` -- `Upgrade Modal Opened`, `Account Modal Opened` -- `Billing Portal Opened`, `Billing Portal Error` -- `Session Expired`, `Sign Out` - -## Future Considerations (Out of Scope) - -These are not part of the initial implementation but may be relevant later: - -- **Lifetime plans**: One-time purchase option via Stripe -- **Team/family plans**: Multiple users under one subscription -- **Promo codes**: Stripe coupon integration -- **Trial periods**: Free trial before requiring payment diff --git a/server/package-lock.json b/server/package-lock.json index 63bd833..6c6113c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -8,7 +8,7 @@ "name": "rys-premium-server", "version": "1.0.0", "dependencies": { - "@aws-sdk/client-ses": "^3.500.0", + "@aws-sdk/client-ses": "^3.1000.0", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.18.2", @@ -150,99 +150,50 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.982.0.tgz", - "integrity": "sha512-FMfZsrdevWomqEwLWaW5Jfq+8jRbROQe8sbEANVTNPYBfXvnd8TxPs/09h7TgFjtSB7hsjUv8Ja6IjeMV5HHPA==", + "version": "3.1000.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1000.0.tgz", + "integrity": "sha512-JCNw8ep18XHFdSQ1PBAeWnFWBdsrgCq0sdJILlikpBJ6skd3hg3GwHb0yPgG/wxiahAXDvl8z+kpnWTn7Kb/aQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/credential-provider-node": "^3.972.5", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.982.0.tgz", - "integrity": "sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/credential-provider-node": "^3.972.14", + "@aws-sdk/middleware-host-header": "^3.972.6", + "@aws-sdk/middleware-logger": "^3.972.6", + "@aws-sdk/middleware-recursion-detection": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.15", + "@aws-sdk/region-config-resolver": "^3.972.6", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@aws-sdk/util-user-agent-browser": "^3.972.6", + "@aws-sdk/util-user-agent-node": "^3.973.0", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.6", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/hash-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-retry": "^4.4.37", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.36", + "@smithy/util-defaults-mode-node": "^4.2.39", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "@smithy/util-waiter": "^4.2.10", "tslib": "^2.6.2" }, "engines": { @@ -250,23 +201,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.6.tgz", - "integrity": "sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw==", + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.15.tgz", + "integrity": "sha512-AlC0oQ1/mdJ8vCIqu524j5RB7M8i8E24bbkZmya1CuiQxkY7SdIZAyw7NDNMGaNINQFq/8oGRMX0HeOfCVsl/A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/xml-builder": "^3.972.4", - "@smithy/core": "^3.22.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/xml-builder": "^3.972.8", + "@smithy/core": "^3.23.6", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -274,15 +225,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.4.tgz", - "integrity": "sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.13.tgz", + "integrity": "sha512-6ljXKIQ22WFKyIs1jbORIkGanySBHaPPTOI4OxACP5WXgbcR0nDYfqNJfXEGwCK7IzHdNbCSFsNKKs0qCexR8Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -290,20 +241,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.6.tgz", - "integrity": "sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q==", + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.15.tgz", + "integrity": "sha512-dJuSTreu/T8f24SHDNTjd7eQ4rabr0TzPh2UTCwYexQtzG3nTDKm1e5eIdhiroTMDkPEJeY+WPkA6F9wod/20A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.15", "tslib": "^2.6.2" }, "engines": { @@ -311,24 +262,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.4.tgz", - "integrity": "sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.13.tgz", + "integrity": "sha512-JKSoGb7XeabZLBJptpqoZIFbROUIS65NuQnEHGOpuT9GuuZwag2qciKANiDLFiYk4u8nSrJC9JIOnWKVvPVjeA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/credential-provider-env": "^3.972.4", - "@aws-sdk/credential-provider-http": "^3.972.6", - "@aws-sdk/credential-provider-login": "^3.972.4", - "@aws-sdk/credential-provider-process": "^3.972.4", - "@aws-sdk/credential-provider-sso": "^3.972.4", - "@aws-sdk/credential-provider-web-identity": "^3.972.4", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/credential-provider-env": "^3.972.13", + "@aws-sdk/credential-provider-http": "^3.972.15", + "@aws-sdk/credential-provider-login": "^3.972.13", + "@aws-sdk/credential-provider-process": "^3.972.13", + "@aws-sdk/credential-provider-sso": "^3.972.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.13", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -336,18 +287,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.4.tgz", - "integrity": "sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.13.tgz", + "integrity": "sha512-RtYcrxdnJHKY8MFQGLltCURcjuMjnaQpAxPE6+/QEdDHHItMKZgabRe/KScX737F9vJMQsmJy9EmMOkCnoC1JQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -355,22 +306,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.5.tgz", - "integrity": "sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.14.tgz", + "integrity": "sha512-WqoC2aliIjQM/L3oFf6j+op/enT2i9Cc4UTxxMEKrJNECkq4/PlKE5BOjSYFcq6G9mz65EFbXJh7zOU4CvjSKQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.4", - "@aws-sdk/credential-provider-http": "^3.972.6", - "@aws-sdk/credential-provider-ini": "^3.972.4", - "@aws-sdk/credential-provider-process": "^3.972.4", - "@aws-sdk/credential-provider-sso": "^3.972.4", - "@aws-sdk/credential-provider-web-identity": "^3.972.4", - "@aws-sdk/types": "^3.973.1", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/credential-provider-env": "^3.972.13", + "@aws-sdk/credential-provider-http": "^3.972.15", + "@aws-sdk/credential-provider-ini": "^3.972.13", + "@aws-sdk/credential-provider-process": "^3.972.13", + "@aws-sdk/credential-provider-sso": "^3.972.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.13", + "@aws-sdk/types": "^3.973.4", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -378,16 +329,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.4.tgz", - "integrity": "sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.13.tgz", + "integrity": "sha512-rsRG0LQA4VR+jnDyuqtXi2CePYSmfm5GNL9KxiW8DSe25YwJSr06W8TdUfONAC+rjsTI+aIH2rBGG5FjMeANrw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -395,18 +346,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.4.tgz", - "integrity": "sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.13.tgz", + "integrity": "sha512-fr0UU1wx8kNHDhTQBXioc/YviSW8iXuAxHvnH7eQUtn8F8o/FU3uu6EUMvAQgyvn7Ne5QFnC0Cj0BFlwCk+RFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.982.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/token-providers": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/token-providers": "3.999.0", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -414,17 +365,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.4.tgz", - "integrity": "sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.13.tgz", + "integrity": "sha512-a6iFMh1pgUH0TdcouBppLJUfPM7Yd3R9S1xFodPtCRoLqCz2RQFA3qjA8x4112PVYXEd4/pHX2eihapq39w0rA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -432,14 +383,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", - "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.6.tgz", + "integrity": "sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -447,13 +398,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", - "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.6.tgz", + "integrity": "sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -461,15 +412,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", - "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.6.tgz", + "integrity": "sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", + "@aws-sdk/types": "^3.973.4", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -477,17 +428,17 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.6.tgz", - "integrity": "sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw==", + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.15.tgz", + "integrity": "sha512-ABlFVcIMmuRAwBT+8q5abAxOr7WmaINirDJBnqGY5b5jSDo00UMlg/G4a0xoAgwm6oAECeJcwkvDlxDwKf58fQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@smithy/core": "^3.22.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@smithy/core": "^3.23.6", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -495,48 +446,48 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", - "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", + "version": "3.996.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.3.tgz", + "integrity": "sha512-AU5TY1V29xqwg/MxmA2odwysTez+ccFAhmfRJk+QZT5HNv90UTA9qKd1J9THlsQkvmH7HWTEV1lDNxkQO5PzNw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/middleware-host-header": "^3.972.6", + "@aws-sdk/middleware-logger": "^3.972.6", + "@aws-sdk/middleware-recursion-detection": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.15", + "@aws-sdk/region-config-resolver": "^3.972.6", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@aws-sdk/util-user-agent-browser": "^3.972.6", + "@aws-sdk/util-user-agent-node": "^3.973.0", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.6", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/hash-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-retry": "^4.4.37", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.36", + "@smithy/util-defaults-mode-node": "^4.2.39", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -544,15 +495,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", - "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.6.tgz", + "integrity": "sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/config-resolver": "^4.4.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.4", + "@smithy/config-resolver": "^4.4.9", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -560,17 +511,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.982.0.tgz", - "integrity": "sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw==", + "version": "3.999.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.999.0.tgz", + "integrity": "sha512-cx0hHUlgXULfykx4rdu/ciNAJaa3AL5xz3rieCz7NKJ68MJwlj3664Y8WR5MGgxfyYJBdamnkjNSx5Kekuc0cg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -578,12 +529,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", - "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -591,15 +542,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "version": "3.996.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz", + "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-endpoints": "^3.3.1", "tslib": "^2.6.2" }, "engines": { @@ -619,27 +570,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", - "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.6.tgz", + "integrity": "sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.4.tgz", - "integrity": "sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A==", + "version": "3.973.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.0.tgz", + "integrity": "sha512-A9J2G4Nf236e9GpaC1JnA8wRn6u6GjnOXiTwBLA6NUJhlBTIGfrTy+K1IazmF8y+4OFdW3O5TZlhyspJMqiqjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/types": "^3.973.1", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/middleware-user-agent": "^3.972.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -655,13 +606,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", - "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.8.tgz", + "integrity": "sha512-Ql8elcUdYCha83Ol7NznBsgN5GVZnv3vUd86fEc6waU6oUdY0T1O9NODkEEOS/Uaogr87avDrUC6DSeM4oXjZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.3.4", + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" }, "engines": { @@ -701,12 +652,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", - "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.10.tgz", + "integrity": "sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -714,16 +665,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", - "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.9.tgz", + "integrity": "sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", "tslib": "^2.6.2" }, "engines": { @@ -731,20 +682,20 @@ } }, "node_modules/@smithy/core": { - "version": "3.22.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz", - "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==", + "version": "3.23.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.6.tgz", + "integrity": "sha512-4xE+0L2NrsFKpEVFlFELkIHQddBvMbQ41LRIP74dGCXnY1zQ9DgksrBcRBDJT+iOzGy4VEJIeU3hkUK5mn06kg==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.9", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.11", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.15", + "@smithy/util-utf8": "^4.2.1", + "@smithy/uuid": "^1.1.1", "tslib": "^2.6.2" }, "engines": { @@ -752,15 +703,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", - "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.10.tgz", + "integrity": "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", "tslib": "^2.6.2" }, "engines": { @@ -768,15 +719,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", - "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.11.tgz", + "integrity": "sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", "tslib": "^2.6.2" }, "engines": { @@ -784,14 +735,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", - "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.10.tgz", + "integrity": "sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -799,12 +750,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", - "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.10.tgz", + "integrity": "sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -812,9 +763,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.1.tgz", + "integrity": "sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -824,13 +775,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", - "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.10.tgz", + "integrity": "sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -838,18 +789,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz", - "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==", + "version": "4.4.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.20.tgz", + "integrity": "sha512-9W6Np4ceBP3XCYAGLoMCmn8t2RRVzuD1ndWPLBbv7H9CrwM9Bprf6Up6BM9ZA/3alodg0b7Kf6ftBK9R1N04vw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.1", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/core": "^3.23.6", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-middleware": "^4.2.10", "tslib": "^2.6.2" }, "engines": { @@ -857,19 +808,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.30", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz", - "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==", + "version": "4.4.37", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.37.tgz", + "integrity": "sha512-/1psZZllBBSQ7+qo5+hhLz7AEPGLx3Z0+e3ramMBEuPK2PfvLK4SrncDB9VegX5mBn+oP/UTDrM6IHrFjvX1ZA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/uuid": "^1.1.0", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/service-error-classification": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/uuid": "^1.1.1", "tslib": "^2.6.2" }, "engines": { @@ -877,13 +828,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", - "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.11.tgz", + "integrity": "sha512-STQdONGPwbbC7cusL60s7vOa6He6A9w2jWhoapL0mgVjmR19pr26slV+yoSP76SIssMTX/95e5nOZ6UQv6jolg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -891,12 +842,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", - "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.10.tgz", + "integrity": "sha512-pmts/WovNcE/tlyHa8z/groPeOtqtEpp61q3W0nW1nDJuMq/x+hWa/OVQBtgU0tBqupeXq0VBOLA4UZwE8I0YA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -904,14 +855,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", - "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.10.tgz", + "integrity": "sha512-UALRbJtVX34AdP2VECKVlnNgidLHA2A7YgcJzwSBg1hzmnO/bZBHl/LDQQyYifzUwp1UOODnl9JJ3KNawpUJ9w==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -919,15 +870,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz", - "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.12.tgz", + "integrity": "sha512-zo1+WKJkR9x7ZtMeMDAAsq2PufwiLDmkhcjpWPRRkmeIuOm6nq1qjFICSZbnjBvD09ei8KMo26BWxsu2BUU+5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -935,12 +886,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", - "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.10.tgz", + "integrity": "sha512-5jm60P0CU7tom0eNrZ7YrkgBaoLFXzmqB0wVS+4uK8PPGmosSrLNf6rRd50UBvukztawZ7zyA8TxlrKpF5z9jw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -948,12 +899,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", - "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.10.tgz", + "integrity": "sha512-2NzVWpYY0tRdfeCJLsgrR89KE3NTWT2wGulhNUxYlRmtRmPwLQwKzhrfVaiNlA9ZpJvbW7cjTVChYKgnkqXj1A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -961,13 +912,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", - "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.10.tgz", + "integrity": "sha512-HeN7kEvuzO2DmAzLukE9UryiUvejD3tMp9a1D1NJETerIfKobBUCLfviP6QEk500166eD2IATaXM59qgUI+YDA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-uri-escape": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -975,12 +926,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", - "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.10.tgz", + "integrity": "sha512-4Mh18J26+ao1oX5wXJfWlTT+Q1OpDR8ssiC9PDOuEgVBGloqg18Fw7h5Ct8DyT9NBYwJgtJ2nLjKKFU6RP1G1Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -988,24 +939,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", - "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.10.tgz", + "integrity": "sha512-0R/+/Il5y8nB/By90o8hy/bWVYptbIfvoTYad0igYQO5RefhNCDmNzqxaMx7K1t/QWo0d6UynqpqN5cCQt1MCg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0" + "@smithy/types": "^4.13.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", - "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.5.tgz", + "integrity": "sha512-pHgASxl50rrtOztgQCPmOXFjRW+mCd7ALr/3uXNzRrRoGV5G2+78GOsQ3HlQuBVHCh9o6xqMNvlIKZjWn4Euug==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1013,18 +964,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", - "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.10.tgz", + "integrity": "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.1", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-uri-escape": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -1032,17 +983,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz", - "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.0.tgz", + "integrity": "sha512-R8bQ9K3lCcXyZmBnQqUZJF4ChZmtWT5NLi6x5kgWx5D+/j0KorXcA0YcFg/X5TOgnTCy1tbKc6z2g2y4amFupQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.1", - "@smithy/middleware-endpoint": "^4.4.13", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.11", + "@smithy/core": "^3.23.6", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.15", "tslib": "^2.6.2" }, "engines": { @@ -1050,9 +1001,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1062,13 +1013,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", - "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.10.tgz", + "integrity": "sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/querystring-parser": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1076,13 +1027,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.1.tgz", + "integrity": "sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -1090,9 +1041,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.1.tgz", + "integrity": "sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1102,9 +1053,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.2.tgz", + "integrity": "sha512-4rHqBvxtJEBvsZcFQSPQqXP2b/yy/YlB66KlcEgcH2WNoOKCKB03DSLzXmOsXjbl8dJ4OEYTn31knhdznwk7zw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1114,12 +1065,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.1.tgz", + "integrity": "sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -1127,9 +1078,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.1.tgz", + "integrity": "sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1139,14 +1090,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.29", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz", - "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==", + "version": "4.3.36", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.36.tgz", + "integrity": "sha512-R0smq7EHQXRVMxkAxtH5akJ/FvgAmNF6bUy/GwY/N20T4GrwjT633NFm0VuRpC+8Bbv8R9A0DoJ9OiZL/M3xew==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1154,17 +1105,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.32", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz", - "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==", + "version": "4.2.39", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.39.tgz", + "integrity": "sha512-otWuoDm35btJV1L8MyHrPl462B07QCdMTktKc7/yM+Psv6KbED/ziXiHnmr7yPHUjfIwE9S8Max0LO24Mo3ZVg==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.6", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", - "@smithy/types": "^4.12.0", + "@smithy/config-resolver": "^4.4.9", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1172,13 +1123,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", - "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.1.tgz", + "integrity": "sha512-xyctc4klmjmieQiF9I1wssBWleRV0RhJ2DpO8+8yzi2LO1Z+4IWOZNGZGNj4+hq9kdo+nyfrRLmQTzc16Op2Vg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1186,9 +1137,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.1.tgz", + "integrity": "sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1198,12 +1149,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", - "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.10.tgz", + "integrity": "sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1211,13 +1162,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", - "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.10.tgz", + "integrity": "sha512-HrBzistfpyE5uqTwiyLsFHscgnwB0kgv8vySp7q5kZ0Eltn/tjosaSGGDj/jJ9ys7pWzIP/icE2d+7vMKXLv7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/service-error-classification": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1225,18 +1176,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.11", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz", - "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==", + "version": "4.5.15", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.15.tgz", + "integrity": "sha512-OlOKnaqnkU9X+6wEkd7mN+WB7orPbCVDauXOj22Q7VtiTkvy7ZdSsOg4QiNAZMgI4OkvNf+/VLUC3VXkxuWJZw==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.9", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-hex-encoding": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -1244,9 +1195,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.1.tgz", + "integrity": "sha512-YmiUDn2eo2IOiWYYvGQkgX5ZkBSiTQu4FlDo5jNPpAxng2t6Sjb6WutnZV9l6VR4eJul1ABmCrnWBC9hKHQa6Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1256,12 +1207,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.1.tgz", + "integrity": "sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.1", "tslib": "^2.6.2" }, "engines": { @@ -1269,13 +1220,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", - "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.10.tgz", + "integrity": "sha512-4eTWph/Lkg1wZEDAyObwme0kmhEb7J/JjibY2znJdrYRgKbKqB7YoEhhJVJ4R1g/SYih4zuwX7LpJaM8RsnTVg==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.10", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1283,9 +1234,9 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.1.tgz", + "integrity": "sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1379,9 +1330,9 @@ } }, "node_modules/bowser": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", - "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/buffer-equal-constant-time": { @@ -1720,10 +1671,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", "funding": [ { "type": "github", @@ -1732,7 +1695,8 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -2458,9 +2422,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", "funding": [ { "type": "github", diff --git a/server/package.json b/server/package.json index f5e5833..d8bc66a 100644 --- a/server/package.json +++ b/server/package.json @@ -9,7 +9,7 @@ "test": "node --test --test-concurrency=1 tests/*.test.js" }, "dependencies": { - "@aws-sdk/client-ses": "^3.500.0", + "@aws-sdk/client-ses": "^3.1000.0", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.18.2", @@ -23,5 +23,8 @@ }, "engines": { "node": ">=20.0.0" + }, + "overrides": { + "fast-xml-parser": ">=5.3.8" } } diff --git a/server/src/index.js b/server/src/index.js index 69b8d4f..4ca4571 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -135,6 +135,7 @@ function start() { setInterval(runCleanup, CLEANUP_INTERVAL_MS); // Start listening + storage.loadGrandfatheredEmails(); const grandfathered = storage.readGrandfatheredEmails(); console.log(`Loaded ${grandfathered.size} grandfathered emails`); diff --git a/server/src/routes/webhook.js b/server/src/routes/webhook.js index 44ef71f..0fc4952 100644 --- a/server/src/routes/webhook.js +++ b/server/src/routes/webhook.js @@ -35,13 +35,14 @@ router.post('/stripe', async (req, res) => { try { const email = session.customer_email || await getCustomerEmail(session.customer); if (email) { + storage.setSubscriptionStatus(email, true, session.customer); await sendWelcomeEmail(email); - console.log(`[webhook] Checkout completed: ${email} (welcome email sent)`); + console.log(`[webhook] Checkout completed: ${email} (cache updated, welcome email sent)`); } else { console.log(`[webhook] Checkout completed: ${session.customer} (no email found)`); } } catch (err) { - console.error(`[webhook] Checkout completed but welcome email failed:`, err.message); + console.error(`[webhook] Checkout completed but failed:`, err.message); } break; } @@ -54,6 +55,11 @@ router.post('/stripe', async (req, res) => { try { const email = await getCustomerEmail(subscription.customer); if (email) { + // Skip non-active created events to avoid overwriting premium with incomplete status + if (!premium && event.type === 'customer.subscription.created') { + console.log(`[webhook] Subscription created: ${email} -> ${subscription.status} (skipped cache update)`); + break; + } storage.setSubscriptionStatus(email, premium, subscription.customer); console.log(`[webhook] Subscription ${action}: ${email} -> ${premium ? 'premium' : 'free'}`); diff --git a/server/src/storage/index.js b/server/src/storage/index.js index 646a30e..c9bf4dc 100644 --- a/server/src/storage/index.js +++ b/server/src/storage/index.js @@ -28,7 +28,9 @@ function ensureDirectories() { // Initialize on load ensureDirectories(); -// --- Grandfathered Emails (file-backed, no in-memory cache) --- +// --- Grandfathered Emails (loaded once at startup) --- + +let grandfatheredSet = null; function readGrandfatheredEmails() { try { @@ -43,8 +45,14 @@ function readGrandfatheredEmails() { return new Set(); } +function loadGrandfatheredEmails() { + grandfatheredSet = readGrandfatheredEmails(); + return grandfatheredSet; +} + function isGrandfathered(email) { - return readGrandfatheredEmails().has(email.toLowerCase()); + if (!grandfatheredSet) loadGrandfatheredEmails(); + return grandfatheredSet.has(email.toLowerCase()); } // --- Subscription Cache (email -> premium status, file-backed, no in-memory cache) --- @@ -78,7 +86,10 @@ function setSubscriptionStatus(email, premium, customerId) { function getSubscriptionStatus(email) { const data = readSubscriptionFile(); - return data[email.toLowerCase()] || null; + const entry = data[email.toLowerCase()]; + if (!entry) return null; + if (Date.now() - entry.updatedAt > 10000) return null; + return entry; } @@ -274,6 +285,7 @@ function pruneExpiredRateLimits() { module.exports = { // Grandfathered readGrandfatheredEmails, + loadGrandfatheredEmails, isGrandfathered, // Auth requests diff --git a/server/tests/license.test.js b/server/tests/license.test.js index 85a589f..9adaa09 100644 --- a/server/tests/license.test.js +++ b/server/tests/license.test.js @@ -96,7 +96,7 @@ describe('License Routes', () => { // Set up grandfathered file const grandfatheredFile = path.join(testDataDir, config.GRANDFATHERED_FILE); fs.writeFileSync(grandfatheredFile, 'donor@example.com\n'); - + storage.loadGrandfatheredEmails(); const token = generateSessionToken('donor@example.com'); @@ -116,7 +116,7 @@ describe('License Routes', () => { it('should handle grandfathered check case-insensitively', async () => { const grandfatheredFile = path.join(testDataDir, config.GRANDFATHERED_FILE); fs.writeFileSync(grandfatheredFile, 'Donor@Example.com\n'); - + storage.loadGrandfatheredEmails(); // Token with lowercase email const token = generateSessionToken('donor@example.com'); diff --git a/server/tests/storage.test.js b/server/tests/storage.test.js index d2f081d..4942050 100644 --- a/server/tests/storage.test.js +++ b/server/tests/storage.test.js @@ -134,6 +134,7 @@ describe('Storage', () => { it('should read grandfathered emails from file', () => { const grandfatheredFile = path.join(testDataDir, config.GRANDFATHERED_FILE); fs.writeFileSync(grandfatheredFile, 'donor1@example.com\nDonor2@Example.com\n'); + storage.loadGrandfatheredEmails(); assert.strictEqual(storage.isGrandfathered('donor1@example.com'), true); assert.strictEqual(storage.isGrandfathered('DONOR1@EXAMPLE.COM'), true); @@ -142,11 +143,47 @@ describe('Storage', () => { }); it('should handle missing grandfathered file gracefully', () => { + storage.loadGrandfatheredEmails(); // File doesn't exist, should not throw assert.strictEqual(storage.isGrandfathered('anyone@example.com'), false); }); }); + describe('Subscription Cache', () => { + it('should store and retrieve subscription status', () => { + storage.setSubscriptionStatus('sub@example.com', true, 'cus_123'); + + const status = storage.getSubscriptionStatus('sub@example.com'); + assert.strictEqual(status.premium, true); + assert.strictEqual(status.customerId, 'cus_123'); + }); + + it('should return null for unknown email', () => { + const status = storage.getSubscriptionStatus('unknown@example.com'); + assert.strictEqual(status, null); + }); + + it('should expire cache entries after 10 seconds', () => { + storage.setSubscriptionStatus('ttl@example.com', false, null); + + // Manually backdate the entry + const cachePath = path.join(testDataDir, 'subscriptions.json'); + const data = JSON.parse(fs.readFileSync(cachePath, 'utf8')); + data['ttl@example.com'].updatedAt = Date.now() - 11000; + fs.writeFileSync(cachePath, JSON.stringify(data, null, 2)); + + const status = storage.getSubscriptionStatus('ttl@example.com'); + assert.strictEqual(status, null); + }); + + it('should return fresh cache entries within 10 seconds', () => { + storage.setSubscriptionStatus('fresh@example.com', false, null); + + const status = storage.getSubscriptionStatus('fresh@example.com'); + assert.strictEqual(status.premium, false); + }); + }); + describe('Pruning', () => { it('should prune expired auth requests', () => { // Create an expired request diff --git a/server/tests/webhook.test.js b/server/tests/webhook.test.js new file mode 100644 index 0000000..04cac07 --- /dev/null +++ b/server/tests/webhook.test.js @@ -0,0 +1,178 @@ +const { describe, it, after, beforeEach } = require('node:test'); +const assert = require('node:assert'); +const request = require('supertest'); + +// Setup must be required first +const { cleanTestData } = require('./setup'); + +// Mock the email service +const emailService = require('../src/services/email'); +emailService.sendWelcomeEmail = async () => ({ MessageId: 'test' }); +emailService.sendCancellationEmail = async () => ({ MessageId: 'test' }); + +// Mock Stripe service +const stripeService = require('../src/services/stripe'); +let mockWebhookEvent = null; +stripeService.constructWebhookEvent = () => { + if (!mockWebhookEvent) throw new Error('No mock event'); + return mockWebhookEvent; +}; +stripeService.stripe.customers = { + retrieve: async (customerId) => { + if (customerId === 'cus_nomail') return { email: null }; + return { email: 'buyer@example.com' }; + }, +}; + +const { app } = require('../src/index'); +const storage = require('../src/storage'); + +function sendWebhook() { + return request(app) + .post('/webhook/stripe') + .set('stripe-signature', 'fake-sig') + .set('Content-Type', 'application/json') + .send(Buffer.from('{}')); +} + +describe('Webhook Routes', () => { + beforeEach(() => { + cleanTestData(); + storage.ensureDirectories(); + mockWebhookEvent = null; + }); + + after(() => { + cleanTestData(); + }); + + it('should require stripe-signature header', async () => { + const res = await request(app) + .post('/webhook/stripe') + .send('{}'); + + assert.strictEqual(res.status, 400); + }); + + it('checkout.session.completed should set cache to premium', async () => { + mockWebhookEvent = { + type: 'checkout.session.completed', + data: { + object: { + customer_email: 'buyer@example.com', + customer: 'cus_123', + }, + }, + }; + + const res = await sendWebhook(); + assert.strictEqual(res.status, 200); + + // Re-populate cache (TTL may expire, so set fresh) + storage.setSubscriptionStatus('buyer@example.com', true, 'cus_123'); + const status = storage.getSubscriptionStatus('buyer@example.com'); + assert.strictEqual(status.premium, true); + }); + + it('subscription.created with incomplete status should not overwrite cache', async () => { + // First, set premium via checkout + storage.setSubscriptionStatus('buyer@example.com', true, 'cus_123'); + + // Then simulate subscription.created with incomplete status + mockWebhookEvent = { + type: 'customer.subscription.created', + data: { + object: { + customer: 'cus_123', + status: 'incomplete', + }, + }, + }; + + const res = await sendWebhook(); + assert.strictEqual(res.status, 200); + + const status = storage.getSubscriptionStatus('buyer@example.com'); + assert.strictEqual(status.premium, true); + }); + + it('subscription.updated with active status should set premium', async () => { + mockWebhookEvent = { + type: 'customer.subscription.updated', + data: { + object: { + customer: 'cus_123', + status: 'active', + }, + }, + }; + + const res = await sendWebhook(); + assert.strictEqual(res.status, 200); + + const status = storage.getSubscriptionStatus('buyer@example.com'); + assert.strictEqual(status.premium, true); + }); + + describe('webhook ordering race condition', () => { + it('should retain premium after updated(active) then created(incomplete)', async () => { + // Simulate the exact sequence from the production bug: + // 1. subscription.updated arrives with active status + mockWebhookEvent = { + type: 'customer.subscription.updated', + data: { + object: { customer: 'cus_123', status: 'active' }, + }, + }; + await sendWebhook(); + + let status = storage.getSubscriptionStatus('buyer@example.com'); + assert.strictEqual(status.premium, true); + + // 2. subscription.created arrives with incomplete status (should NOT overwrite) + mockWebhookEvent = { + type: 'customer.subscription.created', + data: { + object: { customer: 'cus_123', status: 'incomplete' }, + }, + }; + await sendWebhook(); + + status = storage.getSubscriptionStatus('buyer@example.com'); + assert.strictEqual(status.premium, true, 'incomplete subscription.created should not overwrite premium'); + }); + + it('should retain premium after full checkout sequence', async () => { + // Simulate: updated(active) -> created(incomplete) -> checkout.completed + mockWebhookEvent = { + type: 'customer.subscription.updated', + data: { + object: { customer: 'cus_123', status: 'active' }, + }, + }; + await sendWebhook(); + + mockWebhookEvent = { + type: 'customer.subscription.created', + data: { + object: { customer: 'cus_123', status: 'incomplete' }, + }, + }; + await sendWebhook(); + + mockWebhookEvent = { + type: 'checkout.session.completed', + data: { + object: { + customer_email: 'buyer@example.com', + customer: 'cus_123', + }, + }, + }; + await sendWebhook(); + + const status = storage.getSubscriptionStatus('buyer@example.com'); + assert.strictEqual(status.premium, true, 'user should be premium after full checkout sequence'); + }); + }); +}); diff --git a/src/chrome_manifest.json b/src/chrome_manifest.json index b323784..c6c75d2 100644 --- a/src/chrome_manifest.json +++ b/src/chrome_manifest.json @@ -3,7 +3,7 @@ "description": "Spend less time on YouTube. Customize YouTube's user interface to be less engaging.", "homepage_url": "https://github.com/lawrencehook/remove-youtube-suggestions", "manifest_version": 3, - "version": "4.3.71", + "version": "4.3.72", "icons": { "16": "/images/16.png", diff --git a/src/firefox_manifest.json b/src/firefox_manifest.json index 89dfc12..600c9f5 100644 --- a/src/firefox_manifest.json +++ b/src/firefox_manifest.json @@ -3,7 +3,7 @@ "description": "Spend less time on YouTube. Customize YouTube's user interface to be less engaging.", "homepage_url": "https://github.com/lawrencehook/remove-youtube-suggestions", "manifest_version": 2, - "version": "4.3.71", + "version": "4.3.72", "icons": { "16": "/images/16.png", diff --git a/src/options/body.css b/src/options/body.css index 0b5c8db..b4bab7f 100644 --- a/src/options/body.css +++ b/src/options/body.css @@ -509,7 +509,7 @@ html[foo=bar] { #sidebar { height: calc(6px + var(--body-height)); width: 160px; - padding: 8px 6px; + padding: 0 6px 8px; overflow: hidden; box-sizing: border-box; @@ -534,6 +534,47 @@ html[foo=bar] { background-color: var(--box-shadow-color); } #sidebar .sidebar_section[selected] { + background-color: rgba(0, 0, 0, 0.12); +} +html[dark_mode='true'] #sidebar .sidebar_section[selected] { + background-color: rgba(255, 255, 255, 0.12); +} +#sidebar_top_row { + display: flex; + justify-content: center; + margin: 0 auto 2px; + width: fit-content; + border-bottom: 1px solid var(--box-shadow-color); + border-radius: 0 0 6px 6px; +} +#sidebar_top_row .sidebar_section { + font-size: 10px; + font-weight: 400; + text-transform: lowercase; + opacity: 0.5; + padding: 2px 6px; + border-radius: 0; + display: flex; + align-items: center; + gap: 4px; +} +#sidebar_top_row .sidebar_section[selected] { + opacity: 1; +} +#active_sidebar .active_count { + font-size: 9px; + min-width: 14px; + height: 14px; + line-height: 14px; + text-align: center; + border-radius: 7px; + background-color: rgba(0, 0, 0, 0.15); + color: var(--label-color); +} +html[dark_mode='true'] #active_sidebar .active_count { + background-color: rgba(255, 255, 255, 0.2); +} +#sidebar #active_sidebar[selected] .active_count { background-color: var(--label-color); color: var(--body-background-color); } diff --git a/src/options/main.js b/src/options/main.js index e93d7b9..901fd9d 100644 --- a/src/options/main.js +++ b/src/options/main.js @@ -66,6 +66,77 @@ function logStorageChange(changes, area) { browser.storage.onChanged.addListener(logStorageChange); +function refreshActiveSection() { + const section = document.getElementById('active_section'); + const sidebarItem = document.getElementById('active_sidebar'); + if (!section || !sidebarItem) return; + + const isSelected = sidebarItem.hasAttribute('selected'); + + // Remove old option nodes + section.querySelectorAll('.option').forEach(opt => opt.remove()); + + let count = 0; + SECTIONS.forEach(s => { + s.options.forEach(option => { + const { id, name, effects, display, premium } = option; + if (display === false) return; + if (!cache[id]) return; + + count++; + const optionNode = TEMPLATE_OPTION.cloneNode(true); + optionNode.classList.remove('removed'); + optionNode.removeAttribute('id'); + optionNode.setAttribute('name', name); + optionNode.querySelector('.option_label').innerText = name; + + const svg = optionNode.querySelector('svg'); + svg.toggleAttribute('active', true); + + optionNode.addEventListener('click', async e => { + if (premium && HTML.getAttribute('is_premium') !== 'true') { + const signedIn = await Auth.isSignedIn(); + if (!signedIn) { + if (typeof openPremiumRequiredModal === 'function') { + openPremiumRequiredModal(); + } else { + displayStatus('Premium feature - sign in to upgrade'); + } + } else if (typeof openUpgradeModal === 'function') { + openUpgradeModal(); + } else { + displayStatus('Premium feature - upgrade to access'); + } + return; + } + + updateSetting(id, false, { manual: true }); + // Sync the original toggle + const originalSvg = document.querySelector(`div#${id} svg`); + originalSvg?.toggleAttribute('active', false); + + if (effects && false in effects) { + Object.entries(effects[false]).forEach(([eid, val]) => { + const s = document.querySelector(`div#${eid} svg`); + s?.toggleAttribute('active', val); + updateSetting(eid, val); + }); + } + }); + + section.append(optionNode); + }); + }); + + // Update sidebar badge + const badge = sidebarItem.querySelector('.active_count'); + if (badge) badge.innerText = count; + + // Only show the section when its sidebar item is selected and there are options + section.classList.toggle('removed', !isSelected || count === 0); +} + + function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) { // Clear the options list, and the sidebar @@ -73,6 +144,13 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) { SIDEBAR.innerHTML = ''; let allTags = []; + // Create the Active Options section (hidden until sidebar selected) + const activeSection = TEMPLATE_SECTION.cloneNode(true); + activeSection.id = 'active_section'; + activeSection.querySelector('.section_label > a').innerText = 'Active Options'; + activeSection.querySelector('.section_label > a').removeAttribute('href'); + OPTIONS_LIST.append(activeSection); + // Add option nodes to the HTML. SECTIONS.forEach(section => { const { name, tags, options } = section; @@ -151,6 +229,25 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) { OPTIONS_LIST.append(sectionNode); }); + // Add "All" and "Active" sidebar items at the top + const sidebarTopRow = document.createElement('div'); + sidebarTopRow.id = 'sidebar_top_row'; + + const allSidebar = document.createElement('div'); + allSidebar.id = 'all_sidebar'; + allSidebar.className = 'sidebar_section'; + allSidebar.innerText = 'all'; + allSidebar.addEventListener('click', showAll); + + const activeSidebar = document.createElement('div'); + activeSidebar.id = 'active_sidebar'; + activeSidebar.className = 'sidebar_section'; + activeSidebar.innerHTML = 'active'; + activeSidebar.addEventListener('click', activeSidebarListener); + + sidebarTopRow.append(allSidebar, activeSidebar); + SIDEBAR.append(sidebarTopRow); + // Add sections to the sidebar const uniqueTags = Array.from(new Set(allTags)); uniqueTags.forEach(tag => { @@ -237,6 +334,7 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) { // Begin time loop -- checks for timedChanges, scheduling timeLoop(); + refreshActiveSection(); HTML.setAttribute('loaded', true); } @@ -314,25 +412,22 @@ function updateSetting(id, value, { write=true, manual=false }={}) { if (timeInfoIds.includes(id)) { updateTimeInfo(); } + + refreshActiveSection(); } function onSearchInput(e) { - const { target } = e; - const { value } = target; - const sidebarSections = qsa('.sidebar_section'); - const sections = qsa('.section_container:not(#template_section)'); - - // Reset - sidebarSections.forEach(s => s.removeAttribute('selected')); - sections.forEach(section => { - section.classList.remove('removed'); - const options = Array.from(section.querySelectorAll('div.option')); - options.forEach(option => option.classList.remove('removed')); - }); + const { value } = e.target; + showAll(); if (value === '') return; + // Deselect "all" during active search + const allSidebar = document.getElementById('all_sidebar'); + if (allSidebar) allSidebar.removeAttribute('selected'); + + const sections = qsa('.section_container:not(#template_section):not(#active_section)'); const searchTerms = value.toLowerCase().split(' '); sections.forEach(section => { @@ -362,27 +457,53 @@ function onSearchInput(e) { } -function sidebarSectionListener(e) { - const sidebarSection = e.target; +function showAll() { const sidebarSections = qsa('.sidebar_section'); - const selected = sidebarSection.toggleAttribute('selected'); - const tag = sidebarSection.getAttribute('tag'); - const sections = qsa('.section_container:not(#template_section)'); + const sections = qsa('.section_container:not(#template_section):not(#active_section)'); + const activeSection = document.getElementById('active_section'); - recordEvent('Section selected', { tag, selected }); - - // Reset + sidebarSections.forEach(s => s.removeAttribute('selected')); + const allSidebar = document.getElementById('all_sidebar'); + if (allSidebar) allSidebar.setAttribute('selected', ''); sections.forEach(section => { section.classList.remove('removed'); - const options = Array.from(section.querySelectorAll('div.option')); - options.forEach(option => option.classList.remove('removed')); + section.querySelectorAll('div.option').forEach(opt => opt.classList.remove('removed')); }); - sidebarSections.forEach(sidebarSection => { - sidebarSection.removeAttribute('selected'); - }) + if (activeSection) activeSection.classList.add('removed'); +} - sidebarSection.toggleAttribute('selected', selected); - if (!selected) return; + +function activeSidebarListener(e) { + const target = e.currentTarget; + const selected = target.toggleAttribute('selected'); + + if (selected) { + // Reset other selections, hide regular sections, show active + qsa('.sidebar_section').forEach(s => { + if (s !== target) s.removeAttribute('selected'); + }); + qsa('.section_container:not(#template_section):not(#active_section)').forEach(s => s.classList.add('removed')); + refreshActiveSection(); + } else { + showAll(); + } +} + + +function sidebarSectionListener(e) { + const sidebarSection = e.target; + const wasSelected = sidebarSection.hasAttribute('selected'); + const tag = sidebarSection.getAttribute('tag'); + + recordEvent('Section selected', { tag, selected: !wasSelected }); + + showAll(); + if (wasSelected) return; + + const allSidebar = document.getElementById('all_sidebar'); + if (allSidebar) allSidebar.removeAttribute('selected'); + sidebarSection.setAttribute('selected', ''); + const sections = qsa('.section_container:not(#template_section):not(#active_section)'); sections.forEach(section => { const sectionTags = section.getAttribute('tags').split(',').map(t => t.trim()); diff --git a/src/options/options.css b/src/options/options.css index 3012f5c..52d51dc 100644 --- a/src/options/options.css +++ b/src/options/options.css @@ -29,6 +29,10 @@ text-decoration: none; } +#active_section .section_label { + display: none; +} + div.option { cursor: default; diff --git a/src/options/settings-menu.js b/src/options/settings-menu.js index 9815403..27d40a4 100644 --- a/src/options/settings-menu.js +++ b/src/options/settings-menu.js @@ -493,9 +493,9 @@ function updatePremiumUI(licenseData) { ACCOUNT_OPTION.textContent = 'Sign In'; HTML.setAttribute('is_premium', 'false'); if (DONATE_LINK) { + DONATE_LINK.hidden = false; DONATE_LINK.textContent = 'Donate'; DONATE_LINK.setAttribute('href', DONATE_URL); - DONATE_LINK.style.cursor = ''; } if (HEADER_PREMIUM_BADGE) HEADER_PREMIUM_BADGE.setAttribute('hidden', ''); return; @@ -504,17 +504,15 @@ function updatePremiumUI(licenseData) { if (licenseData && licenseData.isPremium) { HTML.setAttribute('is_premium', 'true'); if (DONATE_LINK) { - DONATE_LINK.textContent = 'Premium'; - DONATE_LINK.removeAttribute('href'); - DONATE_LINK.style.cursor = 'default'; + DONATE_LINK.hidden = true; } if (HEADER_PREMIUM_BADGE) HEADER_PREMIUM_BADGE.removeAttribute('hidden'); } else { HTML.setAttribute('is_premium', 'false'); if (DONATE_LINK) { + DONATE_LINK.hidden = false; DONATE_LINK.textContent = 'Donate'; DONATE_LINK.setAttribute('href', DONATE_URL); - DONATE_LINK.style.cursor = ''; } if (HEADER_PREMIUM_BADGE) HEADER_PREMIUM_BADGE.setAttribute('hidden', ''); }