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

@ -19,6 +19,7 @@ import type {
TierComparison,
} from '@d3ro/core/types'
import { Feature } from '@d3ro/core/types'
import { verifySignedLicenseKey, createDefaultTrialPayload } from '@d3ro/core/utils/crypto-license'
const logger = getLogger('license')
@ -46,9 +47,9 @@ const QUOTA_LIMITS: Record<LicenseTier, Partial<Record<Feature, number>>> = {
// 모델별: Haiku 1500 + Sonnet 300 + Opus 50 = 합산 표시
[Feature.PREMIUM_LLM]: 1850,
},
pro_plus: {
// Haiku 무제한 + Sonnet 1500 + Opus 300
},
pro_plus: {},
team: {},
enterprise: {},
}
// ── 기능별 최소 필요 티어 ──────────────────────────────────
@ -76,12 +77,14 @@ const FEATURE_MIN_TIER: Record<Feature, LicenseTier> = {
// ── 클라우드 기능 (로그인 필요 + 일부는 pro gate) ──
[Feature.PREMIUM_LLM]: 'free', // 로그인하면 free도 5회/일, pro는 500/일, pro_plus는 무제한
[Feature.CLOUD_SYNC]: 'free', // 로그인만 하면 free도 사용 가능
[Feature.TEAM_WORKSPACE]: 'team',
}
// ── 클라우드 기능 집합 (익명 로컬 모드에서는 login_required) ──
const CLOUD_FEATURES: Set<Feature> = new Set([
Feature.PREMIUM_LLM,
Feature.CLOUD_SYNC,
Feature.TEAM_WORKSPACE,
])
// ── 히스토리 보존 기간 (일) ────────────────────────────────
@ -89,6 +92,8 @@ export const HISTORY_RETENTION_DAYS: Record<LicenseTier, number> = {
free: 3,
pro: -1,
pro_plus: -1,
team: -1,
enterprise: -1,
}
// ── 티어 순서 (비교용) ────────────────────────────────────
@ -96,6 +101,8 @@ const TIER_ORDER: Record<LicenseTier, number> = {
free: 0,
pro: 1,
pro_plus: 2,
team: 3,
enterprise: 4,
}
/** 오프라인 유예 기간: 30일 */
@ -163,23 +170,74 @@ class LicenseService extends EventEmitter {
const storedActivatedAt = this._readStoredField<number>('licenseActivatedAt')
const storedLastVerified = this._readStoredField<number>('licenseLastVerifiedAt')
const storedGrace = this._readStoredField<number>('licenseOfflineGraceUntil')
const storedIsTrial = this._readStoredField<boolean>('licenseIsTrial')
const storedTrialExpiresAt = this._readStoredField<number>('licenseTrialExpiresAt')
const storedExpiresAt = this._readStoredField<number>('licenseExpiresAt')
const storedCustomerEmail = this._readStoredField<string>('licenseCustomerEmail')
const trialEverStarted = this._readStoredField<boolean>('licenseTrialEverStarted')
if (storedTier && storedTier !== 'free' && storedKey) {
const now = Date.now()
if (storedTier && storedTier !== 'free') {
this._info.tier = storedTier
this._info.licenseKey = storedKey
this._info.licenseKey = storedKey ?? null
this._info.activatedAt = storedActivatedAt ?? null
this._info.lastVerifiedAt = storedLastVerified ?? null
this._info.offlineGraceUntil = storedGrace ?? null
this._info.isTrial = storedIsTrial ?? false
this._info.trialExpiresAt = storedTrialExpiresAt ?? null
this._info.expiresAt = storedExpiresAt ?? null
this._info.customerEmail = storedCustomerEmail ?? null
// 오프라인 유예 기간 체크
if (this._info.offlineGraceUntil && Date.now() > this._info.offlineGraceUntil) {
logger.warn('Offline grace period expired, downgrading to free')
// 1. 체험판 만료 체크 (14-Day Reverse Trial)
if (this._info.isTrial && this._info.trialExpiresAt && now > this._info.trialExpiresAt) {
logger.info('14-day Reverse Trial expired, smoothly transitioning to 100% Free on-device mode')
this._downgradeToFree()
} else if (this._info.expiresAt && now > this._info.expiresAt) {
// 2. 정기 라이센스 만료 체크
logger.warn('License expired, transitioning to Free tier')
this._downgradeToFree()
} else if (this._info.offlineGraceUntil && now > this._info.offlineGraceUntil) {
// 3. 오프라인 유예 기간 체크 (30일 경과)
logger.warn('Offline grace period expired, transitioning to Free tier')
this._downgradeToFree()
}
}
this._initialized = true
logger.info(`LicenseService initialized: tier=${this._info.tier}, machineId=${this._info.machineId.substring(0, 8)}...`)
logger.info(`LicenseService initialized: tier=${this._info.tier}, trial=${this._info.isTrial ?? false}, machineId=${this._info.machineId.substring(0, 8)}...`)
}
/** 14일 Reverse-Trial 시작 (신용카드 불필요) */
startTrial(userEmail: string = 'trial-user@local'): ActivateLicenseResult {
const trialEverStarted = this._readStoredField<boolean>('licenseTrialEverStarted')
if (trialEverStarted) {
return { success: false, tier: this._info.tier, message: 'Reverse trial already used on this machine' }
}
const trialPayload = createDefaultTrialPayload(this._info.machineId, userEmail)
this._info.tier = 'pro_plus'
this._info.licenseKey = `TRIAL-PRO-PLUS-${this._info.machineId.substring(0, 8)}`
this._info.activatedAt = trialPayload.issuedAt
this._info.lastVerifiedAt = trialPayload.issuedAt
this._info.offlineGraceUntil = trialPayload.expiresAt
this._info.isTrial = true
this._info.trialExpiresAt = trialPayload.expiresAt
this._info.customerEmail = trialPayload.customerEmail
this._writeStoredField('licenseTier', 'pro_plus')
this._writeStoredField('licenseKey', this._info.licenseKey)
this._writeStoredField('licenseActivatedAt', this._info.activatedAt)
this._writeStoredField('licenseLastVerifiedAt', this._info.lastVerifiedAt)
this._writeStoredField('licenseOfflineGraceUntil', this._info.offlineGraceUntil)
this._writeStoredField('licenseIsTrial', true)
this._writeStoredField('licenseTrialExpiresAt', this._info.trialExpiresAt)
this._writeStoredField('licenseCustomerEmail', this._info.customerEmail)
this._writeStoredField('licenseTrialEverStarted', true)
this.emit('tier-changed', this.getInfo())
logger.info(`Started 14-day Reverse Trial (Pro+) for machine ${this._info.machineId.substring(0, 8)}`)
return { success: true, tier: 'pro_plus', message: '14-Day Reverse Trial Activated' }
}
// ── Public API ──────────────────────────────────────────
@ -352,9 +410,9 @@ class LicenseService extends EventEmitter {
}
/**
* ( ).
* D3RO-PRO-XXXX-XXXX / D3RO-PLUS-XXXX-XXXX /.
* Payple CloudSync .
* (Ed25519 + ).
* D3RO-LIC-xxx (Ed25519 ) D3RO-PRO-xxx ( )
* Payple/Stripe CloudSync
*/
async activate(key: string): Promise<ActivateLicenseResult> {
const trimmedKey = key.trim()
@ -362,24 +420,28 @@ class LicenseService extends EventEmitter {
return { success: false, tier: 'free', message: 'License key is empty' }
}
const localTier = this._validateKeyLocally(trimmedKey)
if (!localTier) {
return { success: false, tier: 'free', message: 'Invalid license key' }
const verification = verifySignedLicenseKey(trimmedKey, this._info.machineId)
if (!verification.valid) {
return { success: false, tier: 'free', message: verification.message }
}
logger.info(`Local key validated: tier=${localTier}`)
const activatedTier = verification.tier
logger.info(`License validated (${verification.reason}): tier=${activatedTier}`)
const now = Date.now()
this._info = {
...this._info,
tier: localTier,
tier: activatedTier,
licenseKey: trimmedKey,
activatedAt: now,
lastVerifiedAt: now,
offlineGraceUntil: now + OFFLINE_GRACE_PERIOD_MS,
expiresAt: verification.payload?.expiresAt ?? null,
customerEmail: verification.payload?.customerEmail ?? null,
isTrial: verification.payload?.isTrial ?? false,
}
this._persistLicenseInfo()
this._sendToRenderer(IPC_CHANNELS.LICENSE.TIER_CHANGED, this._info)
return { success: true, tier: localTier, message: `Activated ${localTier} license (local)` }
this.emit('tier-changed', this.getInfo())
return { success: true, tier: activatedTier, message: verification.message }
}
/** 라이센스 비활성화 (Free로 복귀) */
@ -419,14 +481,14 @@ class LicenseService extends EventEmitter {
{
feature: Feature.DICTATION,
featureLabel: 'license.feature.dictation',
free: '20/day',
free: 'unlimited (local)',
pro: 'unlimited',
proPlus: 'unlimited',
},
{
feature: Feature.LLM_PROCESS,
featureLabel: 'license.feature.llmProcess',
free: '10/day',
free: 'unlimited (local)',
pro: 'unlimited',
proPlus: 'unlimited',
},
@ -528,34 +590,28 @@ class LicenseService extends EventEmitter {
pro: false,
proPlus: true,
},
{
feature: Feature.TEAM_WORKSPACE,
featureLabel: 'license.feature.teamWorkspace',
free: false,
pro: false,
proPlus: false,
},
]
}
// ── Private helpers ────────────────────────────────────
/**
* (/).
*
* :
* D3RO-PRO-XXXX-XXXX -> pro
* D3RO-PLUS-XXXX-XXXX -> pro_plus
*/
private _validateKeyLocally(key: string): LicenseTier | null {
if (key.startsWith('D3RO-PRO-') && key.length >= 18) {
return 'pro'
}
if (key.startsWith('D3RO-PLUS-') && key.length >= 19) {
return 'pro_plus'
}
return null
}
private _downgradeToFree(): void {
this._info.tier = 'free'
this._info.licenseKey = null
this._info.activatedAt = null
this._info.lastVerifiedAt = null
this._info.offlineGraceUntil = null
this._info.isTrial = false
this._info.trialExpiresAt = null
this._info.expiresAt = null
this._info.customerEmail = null
this._persistLicenseInfo()
}
@ -565,6 +621,10 @@ class LicenseService extends EventEmitter {
this._writeStoredField('licenseActivatedAt', this._info.activatedAt)
this._writeStoredField('licenseLastVerifiedAt', this._info.lastVerifiedAt)
this._writeStoredField('licenseOfflineGraceUntil', this._info.offlineGraceUntil)
this._writeStoredField('licenseIsTrial', this._info.isTrial ?? false)
this._writeStoredField('licenseTrialExpiresAt', this._info.trialExpiresAt ?? null)
this._writeStoredField('licenseExpiresAt', this._info.expiresAt ?? null)
this._writeStoredField('licenseCustomerEmail', this._info.customerEmail ?? null)
}
private _getUsageCount(date: string, feature: Feature): number {
@ -676,3 +736,18 @@ export function getLicenseService(): LicenseService {
export function initLicenseService(): void {
getLicenseService().initialize()
}
export function resetLicenseServiceForTests(): void {
instance = null
try {
const fs = require('fs') as typeof import('fs')
const path = require('path') as typeof import('path')
const { app } = require('electron') as typeof import('electron')
const filePath = path.join(app.getPath('userData'), 'd3ro-license.json')
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
} catch {
// 테스트 격리용 — 파일 없으면 무시
}
}