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.
This commit is contained in:
Lawrence Hook
2026-02-15 18:45:37 -05:00
parent a76f69804b
commit 8825666512
63 changed files with 10364 additions and 482 deletions
+106
View File
@@ -0,0 +1,106 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { loadSourceFile } = require('../setup');
// Load md5.js
const { md5 } = loadSourceFile('shared/md5.js');
describe('MD5', () => {
describe('known test vectors', () => {
// Standard MD5 test vectors from RFC 1321
it('should hash empty string correctly', () => {
assert.strictEqual(md5(''), 'd41d8cd98f00b204e9800998ecf8427e');
});
it('should hash "a" correctly', () => {
assert.strictEqual(md5('a'), '0cc175b9c0f1b6a831c399e269772661');
});
it('should hash "abc" correctly', () => {
assert.strictEqual(md5('abc'), '900150983cd24fb0d6963f7d28e17f72');
});
it('should hash "message digest" correctly', () => {
assert.strictEqual(md5('message digest'), 'f96b697d7cb7938d525a2f31aaf161d0');
});
it('should hash "hello" correctly', () => {
// This is used as a self-test in the source code
assert.strictEqual(md5('hello'), '5d41402abc4b2a76b9719d911017c592');
});
it('should hash lowercase alphabet correctly', () => {
assert.strictEqual(md5('abcdefghijklmnopqrstuvwxyz'), 'c3fcd3d76192e4007dfb496cca67e13b');
});
it('should hash digits correctly', () => {
assert.strictEqual(md5('12345678901234567890123456789012345678901234567890123456789012345678901234567890'),
'57edf4a22be3c955ac49da2e2107b67a');
});
});
describe('password hashing scenarios', () => {
it('should produce consistent hashes for same input', () => {
const password = 'mySecretPassword123';
const hash1 = md5(password);
const hash2 = md5(password);
assert.strictEqual(hash1, hash2);
});
it('should produce different hashes for different inputs', () => {
const hash1 = md5('password1');
const hash2 = md5('password2');
assert.notStrictEqual(hash1, hash2);
});
it('should always return 32 character hex string', () => {
const inputs = ['', 'a', 'abc', 'a very long string with many characters'];
for (const input of inputs) {
const hash = md5(input);
assert.strictEqual(hash.length, 32, `Hash of "${input}" has length ${hash.length}`);
assert.match(hash, /^[0-9a-f]{32}$/, `Hash contains invalid characters: ${hash}`);
}
});
it('should handle special characters', () => {
const hash = md5('!@#$%^&*()_+-=[]{}|;:,.<>?');
assert.strictEqual(hash.length, 32);
assert.match(hash, /^[0-9a-f]{32}$/);
});
it('should handle unicode (though results may vary)', () => {
// MD5 on unicode is implementation-dependent
// Just verify it doesn't crash and returns valid format
const hash = md5('こんにちは');
assert.strictEqual(hash.length, 32);
assert.match(hash, /^[0-9a-f]{32}$/);
});
});
describe('edge cases', () => {
it('should handle strings with null bytes', () => {
const hash = md5('hello\x00world');
assert.strictEqual(hash.length, 32);
});
it('should handle very long strings', () => {
const longString = 'a'.repeat(10000);
const hash = md5(longString);
assert.strictEqual(hash.length, 32);
assert.match(hash, /^[0-9a-f]{32}$/);
});
it('should handle strings at 64-byte boundary', () => {
// MD5 processes in 64-byte blocks
const exactly64 = 'a'.repeat(64);
const hash = md5(exactly64);
assert.strictEqual(hash.length, 32);
});
it('should handle strings just over 64-byte boundary', () => {
const just65 = 'a'.repeat(65);
const hash = md5(just65);
assert.strictEqual(hash.length, 32);
});
});
});