Files
Lawrence Hook 2fe77faf0f version 4.3.72
Squashed commit of the following:

commit 53a18e7987
Author: 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>

commit e2732dee6f
Author: 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>

commit 281c0f1103
Author: 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>

commit 56b119ed98
Author: 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>

commit afed16a38a
Author: 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>

commit 361a8caf84
Author: 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>

commit d857ae8c26
Author: 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>

commit c473314ca5
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] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 49e1a5d9ba
Author: 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>

commit c5734518c4
Author: 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>

commit 6ed3ad0291
Author: 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>

commit 83693504e3
Author: Lawrence Hook <lawrencehook@gmail.com>
Date:   Sun Feb 15 18:54:29 2026 -0500

    update versions

commit 27a6ae6f18
Merge: 3f4b42b d6e642b
Author: Lawrence Hook <lawrencehook@gmail.com>
Date:   Sun Feb 15 18:53:37 2026 -0500

    Merge branch 'released'

commit 3f4b42b2de
Author: 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>

commit 8825666512
Author: 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.

commit a76f69804b
Author: 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>

commit b5d1384ca8
Author: 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>

commit 18b64708b6
Author: Lawrence Hook <lawrencehook@gmail.com>
Date:   Sun Feb 8 20:14:23 2026 -0500

    update version to 4.3.71
2026-03-01 21:57:22 -05:00

179 lines
5.9 KiB
JavaScript

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');
});
});
});