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

@ -7,10 +7,24 @@ import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.
import { createServiceRoleClient } from '../_shared/quota.ts'
interface PortalRequest {
return_url: string
return_url?: unknown
}
function validReturnUrl(value: unknown): string | null {
if (typeof value !== 'string') return null
try {
const url = new URL(value)
if (url.username || url.password) return null
if (url.protocol === 'https:') return url.toString()
if (url.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(url.hostname)) {
return url.toString()
}
return null
} catch {
return null
}
}
// @ts-expect-error — Deno 런타임 전역
Deno.serve(async (req: Request) => {
const preflight = handleCorsPreflightRequest(req)
if (preflight) return preflight
@ -25,8 +39,14 @@ Deno.serve(async (req: Request) => {
try {
const user = await requireUser(req)
const body = (await req.json()) as PortalRequest
const returnUrl = validReturnUrl(body.return_url)
if (!returnUrl) {
return new Response(JSON.stringify({ error: 'invalid_return_url' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// @ts-expect-error — Deno.env
const stripeKey = Deno.env.get('STRIPE_SECRET_KEY') ?? ''
if (!stripeKey) {
return new Response(
@ -44,7 +64,7 @@ Deno.serve(async (req: Request) => {
.maybeSingle()
const customerId = (sub?.stripe_customer_id as string | null | undefined) ?? null
if (!customerId) {
if (!customerId?.startsWith('cus_')) {
return new Response(
JSON.stringify({
error: 'no_customer',
@ -63,16 +83,20 @@ Deno.serve(async (req: Request) => {
},
body: new URLSearchParams({
customer: customerId,
return_url: body.return_url
return_url: returnUrl
})
})
if (!portalResp.ok) {
const errText = await portalResp.text()
throw new Error(`Portal 세션 생성 실패: ${errText}`)
throw new Error('stripe_portal_creation_failed')
}
const data = (await portalResp.json()) as { url: string }
const data = (await portalResp.json()) as { url?: unknown }
if (typeof data.url !== 'string') throw new Error('stripe_portal_response_invalid')
const portalUrl = new URL(data.url)
if (portalUrl.protocol !== 'https:' || !portalUrl.hostname.endsWith('.stripe.com')) {
throw new Error('stripe_portal_response_invalid')
}
return new Response(JSON.stringify({ url: data.url }), {
status: 200,
@ -82,9 +106,8 @@ Deno.serve(async (req: Request) => {
if (err && typeof err === 'object' && 'status' in err && 'message' in err) {
return authErrorResponse(err as AuthError, corsHeaders)
}
const message = err instanceof Error ? err.message : 'Unknown error'
return new Response(JSON.stringify({ error: message }), {
status: 500,
return new Response(JSON.stringify({ error: 'stripe_portal_failed' }), {
status: 502,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}