Changeset dede82e in Klonkt for test


Ignore:
Timestamp:
07/21/2026 03:17:16 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
072a242
Parents:
ec288dc
git-author:
Robin <roboburr@…> (07/21/2026 03:17:13 AM)
git-committer:
Robin <roboburr@…> (07/21/2026 03:17:16 AM)
Message:

Fix: derive the owner's campaign id from the creator token (strict match)

The tester's diagnose line was the smoking gun: config campaign=29148518,
but his 12 memberships (Patreon /identity returns ALL memberships across
every creator, not just this one) contained no 29148518. His real, active
pledges were to 1373144 and 16300989. So the admin campaign_id was simply
wrong, and a wrong id locks out real patrons.

Two corrections:

  • Drop the sole-membership fallback added earlier. It was based on the wrong assumption that /identity is creator-scoped; with global memberships it would grant access to someone backing a DIFFERENT creator. pickCampaign- Membership is strict again: exact campaign match or nothing.
  • Auto-derive the authoritative campaign id from the creator token (GET /campaigns returns the owner's own campaign), prefer it over the typed value, and self-heal the stored config when they differ. The owner no longer has to find/enter the campaign id by hand.

The diagnose line now shows owner=<from token> config=<typed> so a mismatch
is obvious.

Changed files:
src/services/PaidPatreonService.js

  • pickCampaignMembership strict; fetchOwnerCampaignId (creator token); verifyPatron derives + self-heals the campaign id, richer diag

test/paid-patron.test.js

  • strict match tests; fetchOwnerCampaignId with mock fetch

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/paid-patron.test.js

    rec288dc rdede82e  
    3535});
    3636
    37 test('pickCampaignMembership: sole membership is used even if campaign_id is wrong (creator-scoped)', async () => {
     37test('pickCampaignMembership: STRICT — a non-matching campaign_id is refused', async () => {
    3838  const { pickCampaignMembership } = await import('../src/services/PaidPatreonService.js');
     39  // /identity returns ALL the visitor's memberships across every creator, so a
     40  // membership to a DIFFERENT campaign must never grant access here.
    3941  const id = identityWith('42', 'active_patron', 500);
    40   // A typo'd campaign_id must not lock out a real patron: memberships from the
    41   // owner's own client are already their campaign, so fall back to the sole one.
    42   const m = pickCampaignMembership(id, '999');
    43   assert.equal(m.status, 'active_patron');
    44   assert.equal(m.cents, 500);
     42  assert.equal(pickCampaignMembership(id, '999'), null);      // different campaign → no
     43  assert.equal(pickCampaignMembership(id, ''), null);         // no campaign → null
     44  assert.equal(pickCampaignMembership(id, '42').cents, 500);  // exact → yes
    4545});
    4646
    47 test('pickCampaignMembership: multiple memberships + no match → null (no silent grant)', async () => {
     47test('fetchOwnerCampaignId reads the creator campaign from the creator token', async () => {
     48  PP.saveOwnerConfig('s3', { clientId: 'c', clientSecret: 's', campaignId: 'WRONG', accessToken: 'creator-tok', refreshToken: 'r', tokenExp: Math.floor(Date.now() / 1000) + 99999 });
     49  const fetchMock = async (url) => {
     50    if (url.includes('/campaigns')) return { ok: true, json: async () => ({ data: [{ type: 'campaign', id: '16300989' }] }) };
     51    return { ok: false, status: 404, json: async () => ({}) };
     52  };
     53  const id = await PP.fetchOwnerCampaignId('s3', fetchMock);
     54  assert.equal(id, '16300989');
     55});
     56
     57test('pickCampaignMembership: many memberships, only the exact campaign matches', async () => {
    4858  const { pickCampaignMembership } = await import('../src/services/PaidPatreonService.js');
    4959  const id = {
     
    5464    ],
    5565  };
    56   assert.equal(pickCampaignMembership(id, '999'), null);      // ambiguous, refuse
    57   assert.equal(pickCampaignMembership(id, '77').cents, 800);  // exact still works
     66  assert.equal(pickCampaignMembership(id, '999'), null);      // none of them
     67  assert.equal(pickCampaignMembership(id, '42').cents, 300);
     68  assert.equal(pickCampaignMembership(id, '77').cents, 800);
    5869});
    5970
Note: See TracChangeset for help on using the changeset viewer.