feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
82
server/supabase/functions/stripe-webhook/index.test.ts
Normal file
82
server/supabase/functions/stripe-webhook/index.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import {
|
||||
constantTimeEqual,
|
||||
normalizeStripeTier,
|
||||
tierFromStripeSubscriptionPrice,
|
||||
verifyStripeSignature,
|
||||
} from './index.ts'
|
||||
|
||||
function assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
async function stripeHeader(payload: string, secret: string, timestamp: number): Promise<string> {
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign'],
|
||||
)
|
||||
const signature = await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
key,
|
||||
new TextEncoder().encode(`${timestamp}.${payload}`),
|
||||
)
|
||||
const hex = Array.from(new Uint8Array(signature))
|
||||
.map((part) => part.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
return `t=${timestamp},v1=${hex}`
|
||||
}
|
||||
|
||||
Deno.test('Stripe signature verifies exact payload and rejects tampering', async () => {
|
||||
const now = 1_800_000_000
|
||||
const payload = JSON.stringify({ id: 'evt_test', type: 'customer.subscription.updated' })
|
||||
const header = await stripeHeader(payload, 'whsec_test', now)
|
||||
assert(await verifyStripeSignature(payload, header, 'whsec_test', 300, now * 1000), 'valid signature')
|
||||
assert(
|
||||
!(await verifyStripeSignature(`${payload} `, header, 'whsec_test', 300, now * 1000)),
|
||||
'payload mutation must fail',
|
||||
)
|
||||
assert(
|
||||
!(await verifyStripeSignature(payload, header, 'different', 300, now * 1000)),
|
||||
'wrong secret must fail',
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('Stripe signature rejects stale and ambiguous timestamp headers', async () => {
|
||||
const timestamp = 1_800_000_000
|
||||
const payload = '{}'
|
||||
const header = await stripeHeader(payload, 'whsec_test', timestamp)
|
||||
assert(
|
||||
!(await verifyStripeSignature(payload, header, 'whsec_test', 300, (timestamp + 301) * 1000)),
|
||||
'stale signature must fail',
|
||||
)
|
||||
assert(
|
||||
!(await verifyStripeSignature(payload, `${header},t=${timestamp}`, 'whsec_test', 300, timestamp * 1000)),
|
||||
'multiple timestamp fields must fail',
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('Stripe tier is derived from the exact configured subscription price', () => {
|
||||
const prices = { pro: 'price_pro', pro_plus: 'price_pro_plus' }
|
||||
assert(tierFromStripeSubscriptionPrice({
|
||||
items: { data: [{ price: { id: 'price_pro' } }] },
|
||||
}, prices) === 'pro', 'pro price must map to pro')
|
||||
assert(tierFromStripeSubscriptionPrice({
|
||||
items: { data: [{ price: { id: 'price_pro_plus' } }] },
|
||||
}, prices) === 'pro_plus', 'pro plus price must map to pro_plus')
|
||||
assert(tierFromStripeSubscriptionPrice({
|
||||
items: { data: [{ price: { id: 'price_attacker' } }] },
|
||||
}, prices) === null, 'unknown price must fail closed')
|
||||
assert(tierFromStripeSubscriptionPrice({
|
||||
items: { data: [{ price: { id: 'price_pro' } }, { price: { id: 'price_pro_plus' } }] },
|
||||
}, prices) === null, 'ambiguous multi-price subscription must fail closed')
|
||||
assert(normalizeStripeTier('team') === 'pro_plus', 'legacy team alias maps only to pro_plus')
|
||||
assert(normalizeStripeTier('enterprise') === null, 'unknown metadata tier must fail')
|
||||
})
|
||||
|
||||
Deno.test('constant-time comparator rejects length and value mismatch', () => {
|
||||
assert(constantTimeEqual('abc', 'abc'), 'equal strings')
|
||||
assert(!constantTimeEqual('abc', 'abd'), 'different strings')
|
||||
assert(!constantTimeEqual('abc', 'ab'), 'different lengths')
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue