fix(billing): verify the signing scheme the secret actually says it is

Every delivery was refused as a signature mismatch, and nothing in the error
said why.

The provider signs one of two ways. A `whsec_` prefix means Standard Webhooks,
where the secret is a base64 key the verifier decodes; anything else is the
older scheme, where the secret is used as its own raw bytes. Which one a secret
belongs to is decided by when it was created, and every secret created now is
the new one.

The SDK's helper only implements the older scheme — it base64-encodes whatever
it is handed, so a Standard Webhooks secret becomes the literal bytes of the
string including its prefix, and every signature then fails against a key
derived quite differently.

The scheme is now read off the secret rather than configured, so rotating one
cannot put the two out of step. The verifier for the new scheme is the same
library the SDK uses underneath; nothing here hand-rolls crypto.

Tested with a real signature rather than only a rejection. A mismatch is easy to
assert by accident, and a test that only proved bad input is refused would have
passed against the broken version too.

Also teaches the product script to check shape and not just name: a one-time
product where a subscription is wanted cannot be subscribed to at all, and
reporting it as already-there hands back an id that fails at its first use.
This commit is contained in:
Wanjohi
2026-09-19 01:33:26 +03:00
parent 2dfb7f4007
commit 5b90bd0f0c
6 changed files with 146 additions and 41 deletions

View File

@@ -22,6 +22,8 @@
import { Polar } from '@polar-sh/sdk';
import type { PresentmentCurrency } from '@polar-sh/sdk/models/components/presentmentcurrency.js';
type Price = { currency: PresentmentCurrency; amount: number };
/**
* The paid rung of the self-serve ladder.
*
@@ -32,16 +34,29 @@ import type { PresentmentCurrency } from '@polar-sh/sdk/models/components/presen
*
* Amounts are in minor units: 2000 is 20.00.
*/
const PRODUCT = {
name: 'Nestri Pro',
description: 'Cloud sessions on Nestri hardware, and a larger burn allowance.',
recurringInterval: 'month' as const,
prices: [
{ currency: 'usd', amount: 2000 },
{ currency: 'eur', amount: 2000 },
{ currency: 'gbp', amount: 2000 }
] satisfies { currency: PresentmentCurrency; amount: number }[]
};
const PRODUCTS = [
{
env: 'POLAR_FREE_PRODUCT_ID',
name: 'Nestri Free',
description: 'Sessions on hardware you own, and a monthly burn allowance.',
// Recurring, and priced at nothing. It has to be a *subscription* rather
// than a one-time purchase, because a team is put on it outright at
// signup and a one-time product has no subscription to create.
recurringInterval: 'month' as const,
prices: [{ currency: 'usd', amount: 0 }] satisfies Price[]
},
{
env: 'POLAR_PRODUCT_ID',
name: 'Nestri Pro',
description: 'Cloud sessions on Nestri hardware, and a larger burn allowance.',
recurringInterval: 'month' as const,
prices: [
{ currency: 'usd', amount: 2000 },
{ currency: 'eur', amount: 2000 },
{ currency: 'gbp', amount: 2000 }
] satisfies Price[]
}
];
const apply = process.argv.includes('--apply');
const accessToken = process.env.POLAR_ACCESS_TOKEN;
@@ -80,34 +95,49 @@ console.log(`server: ${server}`);
console.log(`organization: ${organization.name} (${organization.id})`);
const existing = await polar.products.list({ organizationId: organization.id, limit: 100 });
const clash = existing.result.items.find((p) => p.name === PRODUCT.name && !p.isArchived);
if (clash) {
console.log(`\nalready there: ${PRODUCT.name} (${clash.id})`);
console.log('nothing to do. Archive it first if you meant to replace it.');
process.exit(0);
}
console.log(`\nwould create: ${PRODUCT.name}, every ${PRODUCT.recurringInterval}`);
for (const price of PRODUCT.prices) {
console.log(` ${price.currency.toUpperCase()} ${(price.amount / 100).toFixed(2)}`);
for (const product of PRODUCTS) {
const clash = existing.result.items.find((p) => p.name === product.name && !p.isArchived);
if (clash) {
// Same name is not the same product. A one-time product where a
// subscription is wanted cannot be subscribed to at all, and reporting
// it as already-there would hand back an id that fails at the first use.
if (clash.recurringInterval !== product.recurringInterval) {
console.log(`\nwrong shape: ${product.name} (${clash.id})`);
console.log(
` wanted every ${product.recurringInterval}, found ${clash.recurringInterval ?? 'one-time'}`
);
console.log(' archive it first, then run this again.');
continue;
}
console.log(`\nalready there: ${product.name}`);
console.log(` ${product.env}=${clash.id}`);
continue;
}
console.log(`\nwould create: ${product.name}, every ${product.recurringInterval}`);
for (const price of product.prices) {
console.log(` ${price.currency.toUpperCase()} ${(price.amount / 100).toFixed(2)}`);
}
if (!apply) {
continue;
}
const created = await polar.products.create({
...(scopedToOrganization ? {} : { organizationId: organization.id }),
name: product.name,
description: product.description,
recurringInterval: product.recurringInterval,
prices: product.prices.map((price) => ({
amountType: 'fixed' as const,
priceCurrency: price.currency,
priceAmount: price.amount
}))
});
console.log(` created: ${product.env}=${created.id}`);
}
if (!apply) {
console.log('\nnothing written. Re-run with --apply to create it.');
process.exit(0);
console.log('\nnothing written. Re-run with --apply to create them.');
}
const created = await polar.products.create({
...(scopedToOrganization ? {} : { organizationId: organization.id }),
name: PRODUCT.name,
description: PRODUCT.description,
recurringInterval: PRODUCT.recurringInterval,
prices: PRODUCT.prices.map((price) => ({
amountType: 'fixed' as const,
priceCurrency: price.currency,
priceAmount: price.amount
}))
});
console.log(`\ncreated: ${created.id}`);
console.log(`set POLAR_PRODUCT_ID=${created.id} for the ${server} deployment.`);