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

@ -2,7 +2,24 @@
// Supabase 연결 정보 SSOT — 데스크톱, 웹, 모바일 모두 여기서 참조.
// Anon key는 클라이언트용 공개 키(RLS 보호)이므로 소스에 포함해도 안전.
interface MobileE2eSupabaseOverride {
url?: unknown
anonKey?: unknown
}
const runtimeOverride = (
globalThis as typeof globalThis & { __D3RO_MOBILE_E2E_SUPABASE__?: MobileE2eSupabaseOverride }
).__D3RO_MOBILE_E2E_SUPABASE__
const runtimeUrl = runtimeOverride?.url
const runtimeAnonKey = runtimeOverride?.anonKey
const validRuntimeOverride = runtimeUrl === 'http://10.0.2.2:55321'
&& typeof runtimeAnonKey === 'string'
&& /^sb_publishable_[A-Za-z0-9_-]{20,}$/.test(runtimeAnonKey)
export const SUPABASE_LOCAL_MOBILE_E2E = validRuntimeOverride
export const SUPABASE_URL =
(validRuntimeOverride ? runtimeUrl : undefined) ||
(typeof process !== 'undefined' &&
(process.env?.NEXT_PUBLIC_SUPABASE_URL ||
process.env?.SUPABASE_URL ||
@ -10,6 +27,7 @@ export const SUPABASE_URL =
'https://llnocwyqvhgwpdjcqqyw.supabase.co'
export const SUPABASE_ANON_KEY =
(validRuntimeOverride ? runtimeAnonKey : undefined) ||
(typeof process !== 'undefined' &&
(process.env?.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
process.env?.SUPABASE_ANON_KEY ||

View file

@ -1712,7 +1712,8 @@ export interface AdMediationAuctionRequest {
}
export interface AdMediationAuctionResult {
winner: AdCreativePayload
/** Null means every configured provider failed or returned no fill. */
winner: AdCreativePayload | null
winningBidEcpm: number
participatingBids: Array<{
networkId: AdNetworkId | string
@ -1878,23 +1879,27 @@ export interface RefundEligibilityResult {
// Phase 18: Multi-PG Billing & Checkout
// ============================================================
export type PaymentGatewayProvider = 'toss' | 'stripe' | 'portone'
export type PaymentGatewayProvider = 'stripe'
export type CheckoutTier = 'pro' | 'pro_plus'
export interface CheckoutSessionParams {
tier: LicenseTier
billingCycle: 'monthly' | 'annual'
currency: 'KRW' | 'USD' | 'EUR'
tier: CheckoutTier
provider: PaymentGatewayProvider
taxId?: string
customerEmail?: string
}
export interface CheckoutSessionResult {
checkoutUrl?: string
clientSecret?: string
orderId: string
amount: number
currency: string
status: 'pending' | 'completed' | 'failed'
checkoutUrl: string
provider: 'stripe'
status: 'pending'
}
export interface VerifyPaymentResult {
success: boolean
activeTier: 'free' | CheckoutTier
}
export interface SubscriptionStatusResult {
tier: 'free' | CheckoutTier
valid: boolean
expiresAt: number | null
}

View file

@ -5,16 +5,16 @@
import { generateKeyPairSync, sign, verify, createPrivateKey, createPublicKey } from 'crypto'
import type { LicenseTier, Feature } from '../types'
/** 기본 내장 Ed25519 공개키 (SPKI PEM 형식) */
export const DEFAULT_LICENSE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
/**
* Ed25519 (SPKI PEM ).
*
*
* . .
*/
export const DEVELOPMENT_LICENSE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA45oxl+jQCX6kR8C582mn9B/qBaX8pvWrsZSXKolM8B4=
-----END PUBLIC KEY-----`
/** 기본 내장 Ed25519 비밀키 (PKCS8 PEM 형식 — 개발/어드민 발급용) */
export const DEFAULT_LICENSE_PRIVATE_KEY = `-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEILnj6K9ZyiJIXTvXwJ8gEow9nkcUmRqdeTp5yurw9CGG
-----END PRIVATE KEY-----`
/** 라이센스 서명 페이로드 */
export interface SignedLicensePayload {
/** 라이센스 고유 ID */
@ -55,6 +55,29 @@ export interface LicenseVerificationResult {
message: string
}
export interface LicenseVerificationOptions {
/** 테스트/개발 fixture 키 허용. production 환경에서는 true여도 무시한다. */
allowDevelopmentKeys?: boolean
/** 호출 환경을 명시한다. 생략하면 NODE_ENV를 사용한다. */
environment?: string
}
const DEVELOPMENT_LICENSES: ReadonlyMap<string, { tier: LicenseTier; licenseId: string; message: string }> = new Map([
['D3RO-PRO-TEST-KEY1', { tier: 'pro', licenseId: 'dev-pro', message: 'Dev Pro License Activated' }],
['D3RO-PLUS-TEST-KEY1', { tier: 'pro_plus', licenseId: 'dev-pro-plus', message: 'Dev Pro+ License Activated' }],
['D3RO-TEAM-TEST-KEY1', { tier: 'team', licenseId: 'dev-team', message: 'Dev Team License Activated' }],
])
function allowsDevelopmentLicenses(options: LicenseVerificationOptions | undefined): boolean {
if (options?.allowDevelopmentKeys !== true) return false
const environment = options.environment ?? process.env.NODE_ENV
return environment === 'development' || environment === 'test'
}
function normalizePublicKeyPem(value: string): string {
return value.trim().replace(/\r\n/g, '\n')
}
/**
* JSON ( )
*/
@ -118,59 +141,28 @@ export function issueSignedLicenseKey(payload: SignedLicensePayload, privateKeyP
export function verifySignedLicenseKey(
licenseKey: string,
currentMachineId?: string,
publicKeyPem: string = DEFAULT_LICENSE_PUBLIC_KEY,
publicKeyPem?: string,
options?: LicenseVerificationOptions,
): LicenseVerificationResult {
const trimmed = licenseKey.trim()
const developmentLicensesAllowed = allowsDevelopmentLicenses(options)
// 1. 레거시 개발자 테스트 키 확인 (하위 호환성)
if (trimmed.startsWith('D3RO-PRO-') && trimmed.length >= 14) {
// 1. 개발 fixture는 정확히 일치하고 test/development에서 명시적으로 허용된 경우만 인정한다.
const developmentLicense = developmentLicensesAllowed ? DEVELOPMENT_LICENSES.get(trimmed) : undefined
if (developmentLicense) {
return {
valid: true,
tier: 'pro',
tier: developmentLicense.tier,
reason: 'dev_key',
payload: {
licenseId: 'dev-pro',
tier: 'pro',
licenseId: developmentLicense.licenseId,
tier: developmentLicense.tier,
customerEmail: 'developer@d3ro.voice',
issuedAt: Date.now(),
expiresAt: null,
machineId: null,
},
message: 'Dev Pro License Activated',
}
}
if (trimmed.startsWith('D3RO-PLUS-') && trimmed.length >= 15) {
return {
valid: true,
tier: 'pro_plus',
reason: 'dev_key',
payload: {
licenseId: 'dev-pro-plus',
tier: 'pro_plus',
customerEmail: 'developer@d3ro.voice',
issuedAt: Date.now(),
expiresAt: null,
machineId: null,
},
message: 'Dev Pro+ License Activated',
}
}
if (trimmed.startsWith('D3RO-TEAM-') && trimmed.length >= 15) {
return {
valid: true,
tier: 'team',
reason: 'dev_key',
payload: {
licenseId: 'dev-team',
tier: 'team',
customerEmail: 'developer@d3ro.voice',
issuedAt: Date.now(),
expiresAt: null,
machineId: null,
},
message: 'Dev Team License Activated',
message: developmentLicense.message,
}
}
@ -185,6 +177,20 @@ export function verifySignedLicenseKey(
}
}
const configuredPublicKey = publicKeyPem ? normalizePublicKeyPem(publicKeyPem) : undefined
const usesDevelopmentPublicKey =
!configuredPublicKey || configuredPublicKey === normalizePublicKeyPem(DEVELOPMENT_LICENSE_PUBLIC_KEY)
if (usesDevelopmentPublicKey && !developmentLicensesAllowed) {
return {
valid: false,
tier: 'free',
reason: 'invalid_signature',
payload: null,
message: 'Production license verification key is not configured',
}
}
const verificationPublicKey = configuredPublicKey || DEVELOPMENT_LICENSE_PUBLIC_KEY
try {
const rawBase64 = trimmed.replace('D3RO-LIC-', '')
const jsonStr = Buffer.from(rawBase64, 'base64url').toString('utf-8')
@ -204,7 +210,7 @@ export function verifySignedLicenseKey(
// 3. Ed25519 디지털 서명 검증
try {
const publicKey = createPublicKey(publicKeyPem)
const publicKey = createPublicKey(verificationPublicKey)
const canonicalData = canonicalizePayload(payload)
const dataBuffer = Buffer.from(canonicalData, 'utf-8')
const signatureBuffer = Buffer.from(signature, 'base64')

View file

@ -108,8 +108,8 @@ export function markdownToSimpleHtml(md: string): string {
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^\- \[.\] (.+)$/gm, '<li>$1</li>')
.replace(/^\- (.+)$/gm, '<li>$1</li>')
.replace(/^- \[.\] (.+)$/gm, '<li>$1</li>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/^\* (.+)$/gm, '<li>$1</li>')
.replace(/\n{2,}/g, '</p><p>')
.replace(/^(?!<[h|l|t|p])/gm, '')