mirror of
https://github.com/lawrencehook/remove-youtube-suggestions.git
synced 2026-07-25 06:54:31 +00:00
13e70cbd58
Squashed commit of the following: commitdbe3eae405Author: Lawrence Hook <lawrencehook@gmail.com> Date: Mon Mar 2 00:13:35 2026 -0500 Add Stripe checkout hint to upgrade modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commitb433727754Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Mar 1 23:48:01 2026 -0500 Add email validation, IP rate limiting, and fix SES parsing bug Add SES email validation with 2s timeout and fail-open behavior to send-magic-link. Add per-IP rate limiting with decrement on verify. Fix SES response parsing path (MailboxValidation.IsValid.ConfidenceVerdict). Add dedicated unit tests for validateEmail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit84b103e46bAuthor: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Mar 1 23:47:51 2026 -0500 Migrate storage layer from flat files to SQLite Replace file-per-record auth requests, file-per-hash rate limits, and JSON blob subscription cache with a single SQLite database via better-sqlite3. Consolidate duplicated email/IP rate limit code into shared helpers. Add missing IP rate limit pruning to cleanup interval. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit84c2ea5586Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Mar 1 21:58:47 2026 -0500 update versions commita4d5a97664Merge:53a18e72fe77faAuthor: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Mar 1 21:58:09 2026 -0500 Merge branch 'released' commit53a18e7987Author: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commite2732dee6fAuthor: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commit281c0f1103Author: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commit56b119ed98Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Feb 22 05:01:00 2026 -0500 Cache grandfathered emails in memory at startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commitafed16a38aAuthor: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commit361a8caf84Author: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commitd857ae8c26Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sat Feb 21 18:24:33 2026 -0500 Update README: replace donations blurb with premium link Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commitc473314ca5Author: 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] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit49e1a5d9baAuthor: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commitc5734518c4Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Feb 15 19:10:05 2026 -0500 Remove TESTING.md and DEPLOYMENT.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit6ed3ad0291Author: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commit83693504e3Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Feb 15 18:54:29 2026 -0500 update versions commit27a6ae6f18Merge:3f4b42bd6e642bAuthor: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Feb 15 18:53:37 2026 -0500 Merge branch 'released' commit3f4b42b2deAuthor: Lawrence Hook <lawrencehook@gmail.com> 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 <noreply@anthropic.com> commit8825666512Author: Lawrence Hook <lawrencehook@gmail.com> 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. commita76f69804bAuthor: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Feb 8 20:45:35 2026 -0500 update description in README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commitb5d1384ca8Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Feb 8 20:44:26 2026 -0500 update "Why I made it" section in README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit18b64708b6Author: Lawrence Hook <lawrencehook@gmail.com> Date: Sun Feb 8 20:14:23 2026 -0500 update version to 4.3.71
193 lines
6.7 KiB
JavaScript
193 lines
6.7 KiB
JavaScript
const { describe, it, after, beforeEach } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Setup must be required first
|
|
const { cleanTestData, testDataDir } = require('./setup');
|
|
|
|
// Now require storage (after env vars are set)
|
|
const storage = require('../src/storage');
|
|
const config = require('../src/config');
|
|
|
|
describe('Storage', () => {
|
|
beforeEach(() => {
|
|
cleanTestData();
|
|
storage.ensureDirectories();
|
|
});
|
|
|
|
after(() => {
|
|
cleanTestData();
|
|
});
|
|
|
|
describe('Auth Requests', () => {
|
|
it('should create and retrieve an auth request', () => {
|
|
const requestId = 'test-request-123';
|
|
const email = 'test@example.com';
|
|
|
|
const created = storage.createAuthRequest(requestId, email);
|
|
|
|
assert.strictEqual(created.request_id, requestId);
|
|
assert.strictEqual(created.email, email);
|
|
assert.strictEqual(created.status, 'pending');
|
|
assert.strictEqual(created.session_token, null);
|
|
|
|
const retrieved = storage.getAuthRequest(requestId);
|
|
|
|
assert.strictEqual(retrieved.request_id, requestId);
|
|
assert.strictEqual(retrieved.email, email);
|
|
});
|
|
|
|
it('should update an auth request', () => {
|
|
const requestId = 'test-request-456';
|
|
storage.createAuthRequest(requestId, 'test@example.com');
|
|
|
|
const updated = storage.updateAuthRequest(requestId, {
|
|
status: 'verified',
|
|
session_token: 'fake-token',
|
|
});
|
|
|
|
assert.strictEqual(updated.status, 'verified');
|
|
assert.strictEqual(updated.session_token, 'fake-token');
|
|
|
|
const retrieved = storage.getAuthRequest(requestId);
|
|
assert.strictEqual(retrieved.status, 'verified');
|
|
});
|
|
|
|
it('should delete an auth request', () => {
|
|
const requestId = 'test-request-789';
|
|
storage.createAuthRequest(requestId, 'test@example.com');
|
|
|
|
const deleted = storage.deleteAuthRequest(requestId);
|
|
assert.strictEqual(deleted, true);
|
|
|
|
const retrieved = storage.getAuthRequest(requestId);
|
|
assert.strictEqual(retrieved, null);
|
|
});
|
|
|
|
it('should return null for non-existent request', () => {
|
|
const retrieved = storage.getAuthRequest('non-existent');
|
|
assert.strictEqual(retrieved, null);
|
|
});
|
|
|
|
it('should return null for expired request', () => {
|
|
const requestId = 'expired-request';
|
|
const email = 'test@example.com';
|
|
|
|
storage.createAuthRequest(requestId, email);
|
|
|
|
// Backdate via direct SQL
|
|
const oldTimestamp = Date.now() - config.REQUEST_ID_EXPIRY_MS - 1000;
|
|
storage.getDb().prepare('UPDATE auth_requests SET created_at = ? WHERE request_id = ?').run(oldTimestamp, requestId);
|
|
|
|
const retrieved = storage.getAuthRequest(requestId);
|
|
assert.strictEqual(retrieved, null);
|
|
});
|
|
});
|
|
|
|
describe('Rate Limiting', () => {
|
|
it('should allow requests under limit', () => {
|
|
const email = 'ratelimit@example.com';
|
|
|
|
for (let i = 0; i < config.RATE_LIMIT_MAX_REQUESTS; i++) {
|
|
const result = storage.checkRateLimit(email);
|
|
assert.strictEqual(result.allowed, true);
|
|
}
|
|
});
|
|
|
|
it('should block requests over limit', () => {
|
|
const email = 'overlimit@example.com';
|
|
|
|
// Use up all allowed requests
|
|
for (let i = 0; i < config.RATE_LIMIT_MAX_REQUESTS; i++) {
|
|
storage.checkRateLimit(email);
|
|
}
|
|
|
|
// Next should be blocked
|
|
const result = storage.checkRateLimit(email);
|
|
assert.strictEqual(result.allowed, false);
|
|
assert.ok(result.resetTime > Date.now());
|
|
});
|
|
|
|
it('should track remaining requests', () => {
|
|
const email = 'remaining@example.com';
|
|
|
|
const first = storage.checkRateLimit(email);
|
|
assert.strictEqual(first.remaining, config.RATE_LIMIT_MAX_REQUESTS - 1);
|
|
|
|
const second = storage.checkRateLimit(email);
|
|
assert.strictEqual(second.remaining, config.RATE_LIMIT_MAX_REQUESTS - 2);
|
|
});
|
|
});
|
|
|
|
describe('Grandfathered Emails', () => {
|
|
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);
|
|
assert.strictEqual(storage.isGrandfathered('donor2@example.com'), true);
|
|
assert.strictEqual(storage.isGrandfathered('notadonor@example.com'), false);
|
|
});
|
|
|
|
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);
|
|
|
|
// Backdate via direct SQL
|
|
const oldTime = Date.now() - 11000;
|
|
storage.getDb().prepare('UPDATE subscription_cache SET updated_at = ? WHERE email = ?').run(oldTime, 'ttl@example.com');
|
|
|
|
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 via createAuthRequest + backdate
|
|
storage.createAuthRequest('old-request', 'test@example.com');
|
|
const oldTimestamp = Date.now() - config.REQUEST_ID_EXPIRY_MS - 1000;
|
|
storage.getDb().prepare('UPDATE auth_requests SET created_at = ? WHERE request_id = ?').run(oldTimestamp, 'old-request');
|
|
|
|
// Create a valid request
|
|
storage.createAuthRequest('valid-request', 'test@example.com');
|
|
|
|
const pruned = storage.pruneExpiredAuthRequests();
|
|
|
|
assert.strictEqual(pruned, 1);
|
|
assert.strictEqual(storage.getAuthRequest('old-request'), null);
|
|
assert.notStrictEqual(storage.getAuthRequest('valid-request'), null);
|
|
});
|
|
});
|
|
});
|