feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped

This commit is contained in:
Yun Chan 2026-08-20 11:12:05 +09:00
parent 5cd1de6859
commit 708e20f747
406 changed files with 42464 additions and 6199 deletions

View file

@ -1,4 +1,5 @@
// src/shared/constants.ts
import type { LicenseTier } from './types'
/** 타이밍 상수 (Speakly 리버스엔지니어링 기반) */
export const TIMING = {
@ -58,7 +59,7 @@ export interface PremiumModelQuota {
readonly period: 'daily' | 'weekly'
}
export const PREMIUM_MODEL_LIMITS: Record<'free' | 'pro' | 'pro_plus', readonly PremiumModelQuota[]> = {
export const PREMIUM_MODEL_LIMITS: Record<LicenseTier, readonly PremiumModelQuota[]> = {
free: [
{ model: 'llm_haiku', i18nKey: 'license.modelHaiku', limit: 250, period: 'weekly' },
],
@ -72,6 +73,16 @@ export const PREMIUM_MODEL_LIMITS: Record<'free' | 'pro' | 'pro_plus', readonly
{ model: 'llm_sonnet', i18nKey: 'license.modelSonnet', limit: 1500, period: 'daily' },
{ model: 'llm_opus', i18nKey: 'license.modelOpus', limit: 300, period: 'daily' },
],
team: [
{ model: 'llm_haiku', i18nKey: 'license.modelHaiku', limit: -1, period: 'daily' },
{ model: 'llm_sonnet', i18nKey: 'license.modelSonnet', limit: 3000, period: 'daily' },
{ model: 'llm_opus', i18nKey: 'license.modelOpus', limit: 600, period: 'daily' },
],
enterprise: [
{ model: 'llm_haiku', i18nKey: 'license.modelHaiku', limit: -1, period: 'daily' },
{ model: 'llm_sonnet', i18nKey: 'license.modelSonnet', limit: -1, period: 'daily' },
{ model: 'llm_opus', i18nKey: 'license.modelOpus', limit: -1, period: 'daily' },
],
} as const
/** 윈도우 크기 */

View file

@ -249,9 +249,22 @@ export function ipcSuccess<T>(data: T): IPCResult<T> {
}
export function ipcError<T>(
code: ErrorCode,
code: ErrorCode | number,
message: string,
details?: Record<string, unknown>
): IPCResult<T> {
return { success: false, error: { code, message, details } }
return { success: false, error: { code: code as ErrorCode, message, details } }
}
export const ok = ipcSuccess
export function err<T = unknown>(code: string | number, message: string, details?: Record<string, unknown>): IPCResult<T> {
return {
success: false,
error: {
code: typeof code === 'number' ? code : ErrorCode.UnknownError,
message: `${code}: ${message}`,
details,
},
}
}

View file

@ -6,3 +6,6 @@ export * from './types'
export * from './errors'
export * from './ipc-channels'
export * from './constants'
export * from './utils/crypto-license'
export * from './utils/pii-redactor'
export * from './utils/secure-memory'

View file

@ -38,6 +38,12 @@ export const IPC_CHANNELS = {
CANCEL_DOWNLOAD: 'stt:cancelDownload',
GET_LANGUAGE: 'stt:getLanguage',
SET_LANGUAGE: 'stt:setLanguage',
GET_PROVIDERS: 'stt:getProviders',
GET_ACTIVE_PROVIDER: 'stt:getActiveProvider',
SET_PROVIDER: 'stt:setProvider',
GET_PROVIDER_CONFIG: 'stt:getProviderConfig',
SET_PROVIDER_CONFIG: 'stt:setProviderConfig',
TEST_CONNECTION: 'stt:testConnection',
// Main → Renderer events
STATUS_CHANGED: 'stt:statusChanged',
DOWNLOAD_PROGRESS: 'stt:downloadProgress'
@ -66,6 +72,8 @@ export const IPC_CHANNELS = {
GET_SERVER_URL: 'llm:getServerUrl',
SET_SERVER_URL: 'llm:setServerUrl',
PULL_MODEL: 'llm:pullModel',
START_SERVER: 'llm:startServer',
CHECK_CONNECTION: 'llm:checkConnection',
// Phase 3.2: Premium LLM (Supabase llm-proxy → Claude)
PREMIUM_GET_STATUS: 'llm:premium:getStatus',
PREMIUM_GET_QUOTA: 'llm:premium:getQuota',
@ -464,6 +472,45 @@ export const IPC_CHANNELS = {
APP: {
DATA_CHANGED: 'app:dataChanged',
},
ONLINE_AUTH: {
REGISTER: 'onlineAuth:register',
LOGIN: 'onlineAuth:login',
LOGOUT: 'onlineAuth:logout',
GET_USER: 'onlineAuth:getUser',
},
// ── Ad Monetization & Mediation ──
ADS: {
GET_CONFIG: 'ads:getConfig',
SET_CONFIG: 'ads:setConfig',
REQUEST_AUCTION: 'ads:requestAuction',
RECORD_IMPRESSION: 'ads:recordImpression',
RECORD_CLICK: 'ads:recordClick',
CLAIM_REWARD: 'ads:claimReward',
GET_REVENUE_STATS: 'ads:getRevenueStats',
GET_SETTLEMENTS: 'ads:getSettlements',
REQUEST_PAYOUT: 'ads:requestPayout',
GET_PUBLISHER_ACCOUNT: 'ads:getPublisherAccount',
SET_PUBLISHER_ACCOUNT: 'ads:setPublisherAccount',
},
// ── Customer Assistance (CA/CS) & Diagnostics ──
SUPPORT: {
GET_DIAGNOSTICS: 'support:getDiagnostics',
QUERY_AI: 'support:queryAI',
CREATE_TICKET: 'support:createTicket',
GET_TICKETS: 'support:getTickets',
CHECK_REFUND: 'support:checkRefund',
},
// ── Multi-PG Payment & Billing ──
PAYMENT: {
CREATE_CHECKOUT_SESSION: 'payment:createCheckoutSession',
VERIFY_PAYMENT: 'payment:verifyPayment',
GET_SUBSCRIPTION_STATUS: 'payment:getSubscriptionStatus',
CANCEL_SUBSCRIPTION: 'payment:cancelSubscription',
},
} as const
// 타입 유틸리티: 채널명 유니온 추출

View file

@ -2,5 +2,16 @@
// Supabase 연결 정보 SSOT — 데스크톱, 웹, 모바일 모두 여기서 참조.
// Anon key는 클라이언트용 공개 키(RLS 보호)이므로 소스에 포함해도 안전.
export const SUPABASE_URL = 'https://llnocwyqvhgwpdjcqqyw.supabase.co'
export const SUPABASE_ANON_KEY = 'sb_publishable_0uo4UYYvUO2y-sVMFdYylA_hHv9qRt5'
export const SUPABASE_URL =
(typeof process !== 'undefined' &&
(process.env?.NEXT_PUBLIC_SUPABASE_URL ||
process.env?.SUPABASE_URL ||
process.env?.VITE_SUPABASE_URL)) ||
'https://llnocwyqvhgwpdjcqqyw.supabase.co'
export const SUPABASE_ANON_KEY =
(typeof process !== 'undefined' &&
(process.env?.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
process.env?.SUPABASE_ANON_KEY ||
process.env?.VITE_SUPABASE_ANON_KEY)) ||
'sb_publishable_0uo4UYYvUO2y-sVMFdYylA_hHv9qRt5'

View file

@ -135,9 +135,60 @@ export interface AudioDeviceChangedEvent {
}
// ============================================================
// STT (로컬 Whisper)
// STT (Multi-provider & 로컬 Whisper)
// ============================================================
export type STTProviderType =
| 'local'
| 'd3ro-cloud'
| 'openai'
| 'groq'
| 'deepgram'
| 'assemblyai'
| 'google'
| 'custom'
export interface STTProviderInfo {
id: STTProviderType
name: string
description: string
badge: string
requiresApiKey: boolean
defaultModel: string
defaultBaseUrl?: string
models: string[]
isCloud: boolean
}
export interface STTProviderConfig {
apiKey?: string
baseUrl?: string
modelId?: string
temperature?: number
}
export interface SetSTTProviderParams {
provider: STTProviderType
}
export interface SetSTTProviderConfigParams {
provider: STTProviderType
config: STTProviderConfig
}
export interface TestSTTConnectionParams {
provider: STTProviderType
apiKey?: string
baseUrl?: string
modelId?: string
}
export interface TestSTTConnectionResult {
success: boolean
latencyMs: number
message: string
}
export enum STTEngineState {
NOT_INSTALLED = 'not-installed',
DOWNLOADING = 'downloading',
@ -152,6 +203,7 @@ export interface STTStatus {
activeModel: string | null
engineVersion: string | null
gpuAccelerated: boolean
activeProvider?: STTProviderType
}
export interface STTModel {
@ -362,18 +414,30 @@ export interface AppConfig {
autoLaunch: boolean
soundEnabled: boolean
selectedDeviceId: string | null
/** 활성 STT 공급자 ('local' | 'openai' | 'groq' | 'deepgram' | 'assemblyai' | 'google' | 'custom') */
sttProvider: STTProviderType
sttModelId: string
sttLanguage: string
/** STT 공급자별 개별 설정 (API 키, 커스텀 모델, Base URL 등) */
sttProviderConfigs: Record<STTProviderType, STTProviderConfig>
/** 클라우드 STT 실패 시 로컬 Whisper 자동 폴백 */
sttFallbackToLocal: boolean
ttsVoiceId: string | null
ttsSpeed: number
ollamaServerUrl: string
onlineApiUrl: string
localModelsDir: string
llmModelId: string | null
/** Ollama REST base URL (LocalLLMService) */
ollamaServerUrl: string
appUsageMode: 'online' | 'local' | null
authToken: string | null
userEmail: string | null
/**
* Phase 3.2: LLM .
* 'local' LocalLLMService (Ollama, , )
* 'premium' PremiumLLMService (Supabase llm-proxy Claude, + )
* LLM .
* 'local' LocalLLMService (Direct GGUF model downloader/runner)
* 'online' OnlineLLMService (.NET Backend API Server)
*/
llmBackend: 'local' | 'premium'
llmBackend: 'local' | 'online'
/**
* .
* 'local' VoiceConversationService (STTLLMTTS , )
@ -915,7 +979,7 @@ export interface CaptionSessionSummary {
// ============================================================
/** 라이센스 티어 */
export type LicenseTier = 'free' | 'pro' | 'pro_plus'
export type LicenseTier = 'free' | 'pro' | 'pro_plus' | 'team' | 'enterprise'
/** 기능 게이팅 대상 */
export enum Feature {
@ -942,6 +1006,8 @@ export enum Feature {
PREMIUM_LLM = 'premium_llm',
/** 멀티 디바이스 동기화 — 로그인만 하면 free도 사용 가능 */
CLOUD_SYNC = 'cloud_sync',
/** 팀 공유 사전 및 지식베이스 (Team/Enterprise 전용) */
TEAM_WORKSPACE = 'team_workspace',
}
/** 라이센스 정보 (electron-store에 저장) */
@ -954,6 +1020,14 @@ export interface LicenseInfo {
lastVerifiedAt: number | null
/** 오프라인 유예 만료 (lastVerifiedAt + 30일) */
offlineGraceUntil: number | null
/** 14일 체험판 여부 */
isTrial?: boolean
/** 체험판 만료 시각 */
trialExpiresAt?: number | null
/** 라이센스 만료 시각 (정기구독/기간제용) */
expiresAt?: number | null
/** 사용자 이메일 */
customerEmail?: string | null
}
/** 일일 사용량 */
@ -1578,3 +1652,249 @@ export interface DiarizeSessionParams {
sessionId: string
numSpeakers?: number
}
// ============================================================
// Phase 16: Free Tier Ad Monetization & Mediation
// ============================================================
export type AdFormat = 'banner_dock' | 'rewarded_video' | 'export_sponsor' | 'audio_chime'
export type AdNetworkId =
| 'ethical_ads'
| 'carbon_ads'
| 'playwire'
| 'unity_ads'
| 'applovin_max'
| 'google_ad_manager'
| 'inmobi'
| 'pubmatic'
| 'mintegral'
| 'direct_sponsor'
| 'overwolf'
| 'liftoff'
export interface AdNetworkConfig {
id: AdNetworkId | string
name: string
enabled: boolean
priority: number
floorEcpm: number
adUnitId?: string
appKey?: string
apiSecret?: string
adapterType: 'rest_json' | 'in_app_bidding' | 'header_bidding_ssp' | 'rewarded_video_sdk' | 'direct_house'
}
export interface AdCreativePayload {
id: string
networkId: AdNetworkId | string
networkName: string
title: string
description: string
ctaText: string
iconUrl?: string
bannerUrl?: string
videoUrl?: string
clickUrl: string
sponsorTag: string
advertiserName: string
bidEcpm: number
format: AdFormat
rewardTokens?: number
durationSeconds?: number
}
export interface AdMediationAuctionRequest {
placement: 'bottom_dock_banner' | 'rewarded_video_quota' | 'export_interstitial' | 'sidebar_sponsor_card'
format: AdFormat
floorEcpm?: number
auctionTimeoutMs?: number
}
export interface AdMediationAuctionResult {
winner: AdCreativePayload
winningBidEcpm: number
participatingBids: Array<{
networkId: AdNetworkId | string
networkName: string
bidEcpm: number
latencyMs: number
status: 'bid' | 'no_bid' | 'timeout' | 'error'
}>
totalAuctionLatencyMs: number
auctionTimestamp: number
}
export interface AdMediationConfig {
networks: AdNetworkConfig[]
rewardTokensAmount: number
rewardCooldownSeconds: number
houseAdFallback: boolean
headerBiddingTimeoutMs: number
defaultFloorEcpm: number
}
export interface AdImpressionEvent {
adId: string
format: AdFormat
network: AdNetworkId | string
networkName?: string
timestamp: number
earnedEcpm?: number
clicked?: boolean
completed?: boolean
}
export interface AdRewardResult {
success: boolean
tokensAdded: number
newTotalQuota: number
nextAvailableAt?: number
rewardId?: string
}
export interface AdSettlementRecord {
id: string
cycleMonth: string // e.g. "2026-08"
networkId: AdNetworkId | string
networkName: string
impressions: number
clicks: number
completions: number
avgEcpm: number
grossRevenueUsd: number
withholdingTaxRate: number // e.g. 0.033 (3.3% KRW)
netRevenueUsd: number
exchangeRateKrw: number // e.g. 1350
netPayoutKrw: number
payoutStatus: 'pending' | 'processing' | 'settled' | 'paid'
paymentMethod: 'bank_wire_krw' | 'paypal' | 'stripe_connect'
beneficiaryAccount: string
settledAt?: number
invoiceNumber?: string
}
export interface AdRevenueStats {
period: string
totalImpressions: number
totalClicks: number
totalCompletions: number
totalRevenueUsd: number
avgEcpm: number
fillRatePercent: number
networkBreakdown: Array<{
network: string
impressions: number
revenueUsd: number
ecpm: number
fillRate: number
}>
settlements: AdSettlementRecord[]
}
export interface PublisherAccountConfig {
accountEmail: string
beneficiaryName: string
payoutBank: string
payoutAccountNumber: string
taxRegistrationNumber?: string
paypalEmail?: string
networksConfigured: number
}
// ============================================================
// Phase 17: Customer Assistance (CA/CS) & Diagnostics
// ============================================================
export type TicketCategory = 'hardware_audio' | 'cuda_gpu' | 'billing_payment' | 'feature_request' | 'general'
export type TicketPriority = 'urgent' | 'high' | 'normal' | 'low'
export type TicketStatus = 'open' | 'in_progress' | 'waiting_customer' | 'resolved' | 'closed'
export interface SystemDiagnosticsPayload {
machineId: string
appVersion: string
platform: string
osRelease: string
activeAudioDevice: string
sttEngine: string
sttModel: string
gpuAccelerated: boolean
vramAvailableMb?: number
recentErrors: Array<{
code: number
timestamp: number
message: string
}>
}
export interface SupportTicket {
id: string
userId?: string
customerEmail: string
category: TicketCategory
priority: TicketPriority
status: TicketStatus
subject: string
description: string
diagnostics?: SystemDiagnosticsPayload
slaDueAt: number
createdAt: number
updatedAt: number
aiSuggestedReply?: string
assignedTo?: string
}
export interface CreateSupportTicketParams {
category: TicketCategory
priority: TicketPriority
subject: string
description: string
customerEmail: string
includeDiagnostics?: boolean
}
export interface AIAssistQuery {
question: string
diagnostics?: SystemDiagnosticsPayload
}
export interface AIAssistResponse {
answer: string
confidence: number
suggestedAction?: string
references?: string[]
}
export interface RefundEligibilityResult {
eligible: boolean
reason: string
purchaseDate: string
tokensConsumedPercent: number
refundableAmount: number
currency: string
}
// ============================================================
// Phase 18: Multi-PG Billing & Checkout
// ============================================================
export type PaymentGatewayProvider = 'toss' | 'stripe' | 'portone'
export interface CheckoutSessionParams {
tier: LicenseTier
billingCycle: 'monthly' | 'annual'
currency: 'KRW' | 'USD' | 'EUR'
provider: PaymentGatewayProvider
taxId?: string
customerEmail?: string
}
export interface CheckoutSessionResult {
checkoutUrl?: string
clientSecret?: string
orderId: string
amount: number
currency: string
status: 'pending' | 'completed' | 'failed'
}

View file

@ -0,0 +1,289 @@
// packages/core/src/utils/crypto-license.ts
// Phase 11+: Ed25519 기반 비대칭 암호화 오프라인 라이센스 생성 및 검증 모듈
// 클라이언트는 공개키(Public Key)만 내장하여 변조 불가능한 로컬 오프라인 검증 수행
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-----
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 */
licenseId: string
/** 라이센스 티어 */
tier: LicenseTier
/** 발급 대상 사용자 이메일 또는 ID */
customerEmail: string
/** 발급 시각 (Unix Timestamp ms) */
issuedAt: number
/** 만료 시각 (Unix Timestamp ms, null = 영구 라이센스) */
expiresAt: number | null
/** 바인딩된 머신 ID (null = 임의 머신 허용) */
machineId: string | null
/** 체험판 여부 */
isTrial?: boolean
/** 커스텀 활성화 기능 오버라이드 목록 */
customFeatures?: Feature[]
/** 팀/조직 ID (Team/Enterprise 티어용) */
teamId?: string
/** 최대 동시 디바이스 허용 수 */
maxDevices?: number
}
/** 서명된 라이센스 토큰 구조 */
export interface SignedLicenseToken {
version: 'v1'
payload: SignedLicensePayload
signature: string // Base64 encoded Ed25519 signature
}
/** 라이센스 검증 결과 */
export interface LicenseVerificationResult {
valid: boolean
tier: LicenseTier
reason: 'valid' | 'invalid_signature' | 'expired' | 'machine_mismatch' | 'corrupted_token' | 'dev_key'
payload: SignedLicensePayload | null
message: string
}
/**
* JSON ( )
*/
function canonicalizePayload(payload: SignedLicensePayload): string {
return JSON.stringify({
licenseId: payload.licenseId,
tier: payload.tier,
customerEmail: payload.customerEmail,
issuedAt: payload.issuedAt,
expiresAt: payload.expiresAt,
machineId: payload.machineId,
isTrial: payload.isTrial ?? false,
teamId: payload.teamId ?? null,
maxDevices: payload.maxDevices ?? 1,
})
}
/**
* [/ ] Ed25519
*/
export function generateLicenseKeyPair(): { publicKeyPem: string; privateKeyPem: string } {
const { publicKey, privateKey } = generateKeyPairSync('ed25519')
return {
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
}
}
/**
* [/ ] Ed25519
* @param payload
* @param privateKeyPem (PKCS8 PEM)
* @returns Base64 (D3RO-LIC-xxx...)
*/
export function issueSignedLicenseKey(payload: SignedLicensePayload, privateKeyPem: string): string {
const privateKey = createPrivateKey(privateKeyPem)
const canonicalData = canonicalizePayload(payload)
const dataBuffer = Buffer.from(canonicalData, 'utf-8')
// Ed25519는 algorithm 파라미터로 null 사용
const signature = sign(null, dataBuffer, privateKey)
const token: SignedLicenseToken = {
version: 'v1',
payload,
signature: signature.toString('base64'),
}
const jsonStr = JSON.stringify(token)
const base64Token = Buffer.from(jsonStr, 'utf-8').toString('base64url')
return `D3RO-LIC-${base64Token}`
}
/**
* [// ]
* @param licenseKey
* @param currentMachineId ID
* @param publicKeyPem (SPKI PEM)
* @returns
*/
export function verifySignedLicenseKey(
licenseKey: string,
currentMachineId?: string,
publicKeyPem: string = DEFAULT_LICENSE_PUBLIC_KEY,
): LicenseVerificationResult {
const trimmed = licenseKey.trim()
// 1. 레거시 개발자 테스트 키 확인 (하위 호환성)
if (trimmed.startsWith('D3RO-PRO-') && trimmed.length >= 14) {
return {
valid: true,
tier: 'pro',
reason: 'dev_key',
payload: {
licenseId: 'dev-pro',
tier: 'pro',
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',
}
}
// 2. 신규 암호화 라이센스 키 파싱 (D3RO-LIC-xxx)
if (!trimmed.startsWith('D3RO-LIC-')) {
return {
valid: false,
tier: 'free',
reason: 'corrupted_token',
payload: null,
message: 'Invalid license key format (must start with D3RO-LIC-)',
}
}
try {
const rawBase64 = trimmed.replace('D3RO-LIC-', '')
const jsonStr = Buffer.from(rawBase64, 'base64url').toString('utf-8')
const token = JSON.parse(jsonStr) as SignedLicenseToken
if (token.version !== 'v1' || !token.payload || !token.signature) {
return {
valid: false,
tier: 'free',
reason: 'corrupted_token',
payload: null,
message: 'Invalid token structure',
}
}
const { payload, signature } = token
// 3. Ed25519 디지털 서명 검증
try {
const publicKey = createPublicKey(publicKeyPem)
const canonicalData = canonicalizePayload(payload)
const dataBuffer = Buffer.from(canonicalData, 'utf-8')
const signatureBuffer = Buffer.from(signature, 'base64')
const isVerified = verify(null, dataBuffer, publicKey, signatureBuffer)
if (!isVerified) {
return {
valid: false,
tier: 'free',
reason: 'invalid_signature',
payload: null,
message: 'Cryptographic signature mismatch. License is corrupted or forged.',
}
}
} catch (cryptoErr) {
return {
valid: false,
tier: 'free',
reason: 'invalid_signature',
payload: null,
message: `Crypto verification failed: ${cryptoErr}`,
}
}
// 4. 만료 기간 체크
const now = Date.now()
if (payload.expiresAt !== null && now > payload.expiresAt) {
return {
valid: false,
tier: 'free',
reason: 'expired',
payload,
message: `License expired on ${new Date(payload.expiresAt).toLocaleDateString()}`,
}
}
// 5. 머신 ID 바인딩 체크 (머신 ID가 지정된 경우)
if (payload.machineId && currentMachineId && payload.machineId !== currentMachineId) {
return {
valid: false,
tier: 'free',
reason: 'machine_mismatch',
payload,
message: 'License is locked to a different machine hardware ID',
}
}
return {
valid: true,
tier: payload.tier,
reason: 'valid',
payload,
message: `Successfully verified ${payload.tier} license for ${payload.customerEmail}`,
}
} catch (err) {
return {
valid: false,
tier: 'free',
reason: 'corrupted_token',
payload: null,
message: `Failed to decode license key: ${err}`,
}
}
}
/**
* 14 Reverse-Trial
*/
export function createDefaultTrialPayload(machineId: string, userEmail: string = 'trial-user@local'): SignedLicensePayload {
const now = Date.now()
const FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1000
return {
licenseId: `trial-${machineId.substring(0, 8)}-${now}`,
tier: 'pro_plus',
customerEmail: userEmail,
issuedAt: now,
expiresAt: now + FOURTEEN_DAYS_MS,
machineId,
isTrial: true,
maxDevices: 1,
}
}

View file

@ -0,0 +1,146 @@
// packages/core/src/utils/pii-redactor.ts
// 2026 차세대 개인정보 보호 (PII/SPI Masking & Redaction Engine)
// CCPA/CPRA, GDPR, HIPAA, 전자금융거래법 준수 — 클라우드 전송 전 민감 데이터 자동 마스킹
export interface RedactionRule {
id: string
name: string
pattern: RegExp
mask: (match: string, ...groups: string[]) => string
}
export interface RedactionResult {
originalText: string
redactedText: string
redactionCount: number
matchedCategories: string[]
tokens: Map<string, string> // Placeholder -> Original Value (for rehydration)
}
/** 한국 및 글로벌 표준 민감 개인정보 정규식 패턴 */
export const DEFAULT_REDACTION_RULES: RedactionRule[] = [
// 1. 한국 주민등록번호 (Resident Registration Number)
{
id: 'kr_rrn',
name: '주민등록번호',
pattern: /\b(\d{6})[- ]?([1-4]\d{6})\b/g,
mask: (_match, p1) => `${p1}-*******`,
},
// 2. 신용카드 번호 (Credit Card Number: 13~16자리)
{
id: 'credit_card',
name: '신용카드번호',
pattern: /\b(?:\d{4}[- ]?){3}\d{4}\b|\b\d{15,16}\b/g,
mask: (match) => {
const digits = match.replace(/\D/g, '')
if (digits.length >= 15) {
return `${digits.slice(0, 4)}-****-****-${digits.slice(-4)}`
}
return '****-****-****-****'
},
},
// 3. 한국 휴대전화 및 일반 전화번호 (Phone Number)
{
id: 'phone_number',
name: '전화번호',
pattern: /\b(01[016789])[- ]?(\d{3,4})[- ]?(\d{4})\b/g,
mask: (_match, p1, _p2, p3) => `${p1}-****-${p3}`,
},
// 4. 이메일 주소 (Email Address)
{
id: 'email',
name: '이메일',
pattern: /\b([a-zA-Z0-9_.+-]+)@([a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)\b/g,
mask: (_match, p1, p2) => {
const visible = p1.length > 2 ? `${p1.slice(0, 2)}***` : `${p1[0]}*`
return `${visible}@${p2}`
},
},
// 5. 미국 사회보장번호 (US Social Security Number)
{
id: 'us_ssn',
name: 'SSN (미국 사회보장번호)',
pattern: /\b(\d{3})[- ]?(\d{2})[- ]?(\d{4})\b/g,
mask: (_match, _p1, _p2, p3) => `***-**-${p3}`,
},
// 6. 한국 계좌번호 패턴 (일반적인 10~14자리 숫자 하이픈 조합)
{
id: 'bank_account',
name: '은행 계좌번호',
pattern: /\b(\d{3,6})[- ](\d{2,6})[- ](\d{3,6})\b/g,
mask: (_match, p1, _p2, p3) => `${p1}-******-${p3}`,
},
]
/**
* (PII)
* @param text
* @param mode 'mask' ( ) | 'tokenize' ( )
* @param enabledRules ID ( )
*/
export function redactPII(
text: string,
mode: 'mask' | 'tokenize' = 'mask',
enabledRules?: string[]
): RedactionResult {
if (!text || typeof text !== 'string') {
return {
originalText: text || '',
redactedText: text || '',
redactionCount: 0,
matchedCategories: [],
tokens: new Map(),
}
}
let redacted = text
let totalCount = 0
const categories = new Set<string>()
const tokens = new Map<string, string>()
let tokenCounter = 0
const activeRules = enabledRules
? DEFAULT_REDACTION_RULES.filter((r) => enabledRules.includes(r.id))
: DEFAULT_REDACTION_RULES
for (const rule of activeRules) {
redacted = redacted.replace(rule.pattern, (match, ...groups) => {
totalCount++
categories.add(rule.name)
if (mode === 'tokenize') {
tokenCounter++
const placeholder = `[REDACTED_${rule.id.toUpperCase()}_${tokenCounter}]`
tokens.set(placeholder, match)
return placeholder
}
return rule.mask(match, ...groups)
})
}
return {
originalText: text,
redactedText: redacted,
redactionCount: totalCount,
matchedCategories: Array.from(categories),
tokens,
}
}
/**
* (Rehydration)
* LLM
*/
export function rehydrateText(
redactedText: string,
tokens: Map<string, string>
): string {
if (!redactedText || tokens.size === 0) return redactedText
let result = redactedText
tokens.forEach((originalValue, placeholder) => {
result = result.split(placeholder).join(originalValue)
})
return result
}

View file

@ -0,0 +1,48 @@
// packages/core/src/utils/secure-memory.ts
// Zero Data Retention (ZDR) & Biometric Audio Memory Wiper
// 음성 바이오메트릭 데이터 메모리 상주 방지 — 처리 완료/취소/에러 시 오디오 버퍼 0으로 즉시 초기화
/**
* Node.js Buffer Uint8Array의 0 (Zero-fill)
*/
export function secureZeroBuffer(buffer: Buffer | Uint8Array | null | undefined): void {
if (!buffer) return
try {
if (typeof buffer.fill === 'function') {
buffer.fill(0)
} else {
for (let i = 0; i < buffer.length; i++) {
buffer[i] = 0
}
}
} catch {
// Silent guard against detached buffers
}
}
/**
* Float32Array PCM
*/
export function secureZeroFloat32Array(array: Float32Array | null | undefined): void {
if (!array) return
try {
array.fill(0)
} catch {
// Silent guard
}
}
/**
* (Zero-trace)
*/
export function secureWipeAudioChunks(chunks: Array<Buffer | Uint8Array | Float32Array> | null | undefined): void {
if (!chunks || !Array.isArray(chunks)) return
for (const chunk of chunks) {
if (chunk instanceof Float32Array) {
secureZeroFloat32Array(chunk)
} else {
secureZeroBuffer(chunk)
}
}
chunks.length = 0
}