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
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:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -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 {
|
||||
// 테스트 격리용 — 파일 없으면 무시
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue