feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

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