@designbasekorea/figma-license-core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,6 +4,9 @@ exports.LicenseErrorCode = void 0;
4
4
  (function (LicenseErrorCode) {
5
5
  LicenseErrorCode["ACTIVATION_LIMIT_REACHED"] = "ACTIVATION_LIMIT_REACHED";
6
6
  LicenseErrorCode["INVALID_LICENSE"] = "INVALID_LICENSE";
7
+ LicenseErrorCode["LICENSE_EXPIRED"] = "LICENSE_EXPIRED";
8
+ LicenseErrorCode["LICENSE_REVOKED"] = "LICENSE_REVOKED";
9
+ LicenseErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
7
10
  LicenseErrorCode["PRODUCT_MISMATCH"] = "PRODUCT_MISMATCH";
8
11
  LicenseErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
9
12
  LicenseErrorCode["STORAGE_ERROR"] = "STORAGE_ERROR";
@@ -27,10 +30,18 @@ function asText(input) {
27
30
  if (typeof input === 'string') {
28
31
  return input;
29
32
  }
30
- if (typeof input === 'object' && 'message' in input) {
31
- const maybeMessage = input.message;
32
- if (typeof maybeMessage === 'string') {
33
- return maybeMessage;
33
+ if (typeof input === 'object') {
34
+ const objectInput = input;
35
+ if (objectInput.raw) {
36
+ return asText(objectInput.raw);
37
+ }
38
+ if (objectInput.body) {
39
+ return asText(objectInput.body);
40
+ }
41
+ const fields = [objectInput.message, objectInput.errorCode, objectInput.code, objectInput.status]
42
+ .filter((value) => typeof value === 'string' && value.trim() !== '');
43
+ if (fields.length > 0) {
44
+ return fields.join(' ');
34
45
  }
35
46
  }
36
47
  try {
@@ -48,13 +59,32 @@ function inferCode(text) {
48
59
  || normalized.includes('already activated on maximum')) {
49
60
  return exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED;
50
61
  }
62
+ if (normalized.includes('rate limited')
63
+ || normalized.includes('rate_limited')
64
+ || normalized.includes('too many requests')
65
+ || normalized.includes('too_many_requests')
66
+ || normalized.includes('429')) {
67
+ return exports.LicenseErrorCode.RATE_LIMITED;
68
+ }
69
+ if (normalized.includes('expired') || normalized.includes('expiration') || normalized.includes('license_expired')) {
70
+ return exports.LicenseErrorCode.LICENSE_EXPIRED;
71
+ }
72
+ if (normalized.includes('revoked')
73
+ || normalized.includes('license_revoked')
74
+ || normalized.includes('cancelled')
75
+ || normalized.includes('canceled')) {
76
+ return exports.LicenseErrorCode.LICENSE_REVOKED;
77
+ }
51
78
  if (normalized.includes('invalid license')
52
79
  || normalized.includes('not found')
53
80
  || normalized.includes('not valid')
54
81
  || normalized.includes('license key is invalid')) {
55
82
  return exports.LicenseErrorCode.INVALID_LICENSE;
56
83
  }
57
- if (normalized.includes('product mismatch') || normalized.includes('wrong product')) {
84
+ if (normalized.includes('product mismatch')
85
+ || normalized.includes('product_mismatch')
86
+ || normalized.includes('product_not_allowed')
87
+ || normalized.includes('wrong product')) {
58
88
  return exports.LicenseErrorCode.PRODUCT_MISMATCH;
59
89
  }
60
90
  if (normalized.includes('network')
@@ -75,7 +105,9 @@ function mapLicenseError(error, fallbackMessage = '라이선스 처리 중 알
75
105
  const text = asText(error);
76
106
  const message = text.trim() || fallbackMessage;
77
107
  const code = inferCode(message);
78
- return new LicenseCoreError(code, message, error, code === exports.LicenseErrorCode.NETWORK_ERROR || code === exports.LicenseErrorCode.SERVER_ERROR);
108
+ return new LicenseCoreError(code, message, error, code === exports.LicenseErrorCode.NETWORK_ERROR
109
+ || code === exports.LicenseErrorCode.SERVER_ERROR
110
+ || code === exports.LicenseErrorCode.RATE_LIMITED);
79
111
  }
80
112
  function isActivationLimitReached(activationLimit, activationUsage) {
81
113
  if (activationLimit <= 0) {
@@ -87,10 +119,19 @@ function isActivationLimitReached(activationLimit, activationUsage) {
87
119
  const STORAGE_KEYS = {
88
120
  paymentStatus: 'paymentStatus',
89
121
  licenseKey: 'licenseKey',
122
+ instanceId: 'instanceId',
90
123
  activationLimit: 'activationLimit',
91
124
  activationUsage: 'activationUsage',
92
125
  usageCount: 'usageCount',
126
+ previousUsageCount: 'previousUsageCount',
127
+ usageWindowKey: 'usageWindowKey',
128
+ planTier: 'planTier',
93
129
  lastVerifiedAt: 'lastVerifiedAt',
130
+ licenseStatus: 'licenseStatus',
131
+ licenseSource: 'licenseSource',
132
+ accessScope: 'accessScope',
133
+ expiresAt: 'expiresAt',
134
+ validationMessage: 'validationMessage',
94
135
  };
95
136
  const DEFAULT_USAGE_LIMIT = 20;
96
137
  const MASTER_CODE_ACTIVATION_LIMIT = 999999;
@@ -109,6 +150,31 @@ function toNonNegativeNumber(value, fallback = 0) {
109
150
  function toText(value) {
110
151
  return typeof value === 'string' ? value : '';
111
152
  }
153
+ function validationMessage(result) {
154
+ if (result.message) {
155
+ return result.message;
156
+ }
157
+ const status = (result.licenseStatus || result.errorCode || '').toLowerCase();
158
+ switch (status) {
159
+ case 'revoked':
160
+ case 'license_revoked':
161
+ return '이 멤버십 라이선스는 철회되었습니다.';
162
+ case 'expired':
163
+ case 'license_expired':
164
+ return '멤버십 라이선스가 만료되었습니다. YouTube 커뮤니티에서 이번 달 키를 확인해 주세요.';
165
+ case 'not_started':
166
+ return '이 멤버십 라이선스는 아직 시작되지 않았습니다.';
167
+ case 'rate_limited':
168
+ case 'rate_limited_error':
169
+ return '라이선스 확인 요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.';
170
+ case 'product_not_allowed':
171
+ return '이 라이선스는 현재 플러그인에서 사용할 수 없습니다.';
172
+ case 'network_error':
173
+ return '라이선스 서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.';
174
+ default:
175
+ return '라이선스가 더 이상 유효하지 않습니다.';
176
+ }
177
+ }
112
178
  function toPaymentStatus(value) {
113
179
  if (value === 'PAID' || value === 'UNPAID' || value === 'FREE') {
114
180
  return value;
@@ -118,10 +184,77 @@ function toPaymentStatus(value) {
118
184
  function normalizeProductId(productId) {
119
185
  return productId.trim().toLowerCase();
120
186
  }
187
+ function normalizeTier(tier) {
188
+ return tier.trim() || 'FREE';
189
+ }
190
+ function normalizeUsageReset(reset) {
191
+ if (reset === 'daily' || reset === 'weekly' || reset === 'monthly' || reset === 'none') {
192
+ return reset;
193
+ }
194
+ return 'none';
195
+ }
196
+ function toDateKey(date, period) {
197
+ if (period === 'none') {
198
+ return 'lifetime';
199
+ }
200
+ const year = date.getUTCFullYear();
201
+ const month = `${date.getUTCMonth() + 1}`.padStart(2, '0');
202
+ const day = `${date.getUTCDate()}`.padStart(2, '0');
203
+ if (period === 'monthly') {
204
+ return `${year}-${month}`;
205
+ }
206
+ if (period === 'weekly') {
207
+ // ISO weeks start on Monday and keep the week-year at the Thursday of
208
+ // the current week. Using UTC avoids a reset moving at local DST changes.
209
+ const dayOfWeek = date.getUTCDay() || 7;
210
+ const thursday = new Date(Date.UTC(year, date.getUTCMonth(), date.getUTCDate()));
211
+ thursday.setUTCDate(thursday.getUTCDate() + (4 - dayOfWeek));
212
+ const weekYear = thursday.getUTCFullYear();
213
+ const firstThursday = new Date(Date.UTC(weekYear, 0, 4));
214
+ const firstDay = firstThursday.getUTCDay() || 7;
215
+ firstThursday.setUTCDate(firstThursday.getUTCDate() + (4 - firstDay));
216
+ const week = 1 + Math.round((thursday.getTime() - firstThursday.getTime()) / 604800000);
217
+ return `${weekYear}-W${String(week).padStart(2, '0')}`;
218
+ }
219
+ return `${year}-${month}-${day}`;
220
+ }
221
+ function normalizePolicy(config) {
222
+ const rawPolicy = config.policy || {};
223
+ const freeTier = normalizeTier(rawPolicy.freeTier || 'FREE');
224
+ const paidTier = normalizeTier(rawPolicy.paidTier || 'PAID');
225
+ const defaultTier = normalizeTier(rawPolicy.defaultTier || freeTier);
226
+ const usageByTier = {
227
+ [freeTier]: {
228
+ limit: Math.max(config.usageLimit ?? DEFAULT_USAGE_LIMIT, 0),
229
+ reset: 'none',
230
+ },
231
+ [paidTier]: {
232
+ limit: 'unlimited',
233
+ reset: 'none',
234
+ },
235
+ ...(rawPolicy.usageByTier || {}),
236
+ };
237
+ Object.keys(usageByTier).forEach((tier) => {
238
+ usageByTier[tier] = {
239
+ limit: usageByTier[tier].limit === 'unlimited'
240
+ ? 'unlimited'
241
+ : Math.max(toNonNegativeNumber(usageByTier[tier].limit, 0), 0),
242
+ reset: normalizeUsageReset(usageByTier[tier].reset),
243
+ };
244
+ });
245
+ return {
246
+ defaultTier,
247
+ freeTier,
248
+ paidTier,
249
+ usageByTier,
250
+ featuresByTier: rawPolicy.featuresByTier || {},
251
+ };
252
+ }
121
253
  class LicenseCore {
122
254
  constructor(config, deps) {
123
255
  this.config = config;
124
256
  this.deps = deps;
257
+ this.usageMutation = Promise.resolve();
125
258
  if (!config.productId.trim()) {
126
259
  throw new Error('LicenseConfig.productId is required.');
127
260
  }
@@ -131,14 +264,117 @@ class LicenseCore {
131
264
  if (!config.instanceName.trim()) {
132
265
  throw new Error('LicenseConfig.instanceName is required.');
133
266
  }
134
- this.usageLimit = Math.max(config.usageLimit ?? DEFAULT_USAGE_LIMIT, 0);
267
+ this.policy = normalizePolicy(config);
135
268
  this.storagePrefix = config.storagePrefix?.trim() || `${config.instanceName}:${config.productId}`;
136
269
  }
137
270
  async initialize() {
138
- return this.loadState();
271
+ let state = await this.getStatus();
272
+ if (state.paymentStatus === 'PAID' && state.licenseKey && state.licenseKey !== this.config.masterCode) {
273
+ try {
274
+ const validateResult = this.deps.transport.validate
275
+ ? await this.deps.transport.validate({
276
+ licenseKey: state.licenseKey,
277
+ productId: this.config.productId,
278
+ instanceName: this.config.instanceName,
279
+ instanceId: state.instanceId,
280
+ })
281
+ : await this.deps.transport.verify({
282
+ licenseKey: state.licenseKey,
283
+ productId: this.config.productId,
284
+ instanceName: this.config.instanceName,
285
+ });
286
+ if (!validateResult.valid) {
287
+ state = await this.markValidationFailure(state, validationMessage(validateResult), validateResult.errorCode || validateResult.licenseStatus);
288
+ }
289
+ else {
290
+ state = await this.applyValidationResult(state, validateResult);
291
+ }
292
+ }
293
+ catch (error) {
294
+ const mappedError = mapLicenseError(error, '라이선스 검증 요청에 실패했습니다.');
295
+ state = await this.markValidationFailure(state, mappedError.message, mappedError.code);
296
+ }
297
+ }
298
+ return this.applyUsageWindowResetIfNeeded(state);
299
+ }
300
+ async resetLicenseState() {
301
+ const current = await this.getStatus();
302
+ const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
303
+ const previousUsageCount = current.previousUsageCount ?? 0;
304
+ const nextState = {
305
+ ...current,
306
+ paymentStatus: 'FREE',
307
+ planTier: this.policy.freeTier,
308
+ usageWindowKey: toDateKey(new Date(), freeReset),
309
+ licenseKey: '',
310
+ instanceId: '',
311
+ activationLimit: 0,
312
+ activationUsage: 0,
313
+ usageCount: previousUsageCount,
314
+ lastVerifiedAt: undefined,
315
+ licenseStatus: undefined,
316
+ licenseSource: undefined,
317
+ accessScope: undefined,
318
+ expiresAt: undefined,
319
+ validationMessage: undefined,
320
+ };
321
+ await this.persistState(nextState);
322
+ return nextState;
323
+ }
324
+ async applyValidationResult(current, result) {
325
+ const nextState = {
326
+ ...current,
327
+ paymentStatus: 'PAID',
328
+ activationLimit: result.activationLimit ?? current.activationLimit,
329
+ activationUsage: result.activationUsage ?? current.activationUsage,
330
+ licenseStatus: result.licenseStatus || 'active',
331
+ licenseSource: result.licenseSource || current.licenseSource,
332
+ accessScope: result.accessScope || current.accessScope,
333
+ expiresAt: result.expiresAt ?? current.expiresAt,
334
+ validationMessage: result.message || undefined,
335
+ lastVerifiedAt: new Date().toISOString(),
336
+ };
337
+ await this.persistState(nextState);
338
+ return nextState;
339
+ }
340
+ async markValidationFailure(current, message, errorCode) {
341
+ const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
342
+ const nextState = {
343
+ ...current,
344
+ paymentStatus: 'UNPAID',
345
+ planTier: this.policy.freeTier,
346
+ usageWindowKey: toDateKey(new Date(), freeReset),
347
+ activationLimit: 0,
348
+ activationUsage: 0,
349
+ usageCount: current.previousUsageCount ?? current.usageCount,
350
+ licenseStatus: errorCode || 'invalid',
351
+ accessScope: undefined,
352
+ expiresAt: undefined,
353
+ validationMessage: message,
354
+ lastVerifiedAt: new Date().toISOString(),
355
+ };
356
+ await this.persistState(nextState);
357
+ return nextState;
139
358
  }
140
359
  async getStatus() {
141
- return this.loadState();
360
+ const state = await this.loadState();
361
+ return this.applyUsageWindowResetIfNeeded(state);
362
+ }
363
+ async getLicenseSummary() {
364
+ const state = await this.getStatus();
365
+ return {
366
+ paymentStatus: state.paymentStatus,
367
+ planTier: this.resolveTier(state),
368
+ licenseKey: state.licenseKey,
369
+ hasLicenseKey: Boolean(state.licenseKey),
370
+ isLicenseRegistered: this.isLicenseRegisteredState(state),
371
+ licenseStatus: state.licenseStatus,
372
+ licenseSource: state.licenseSource,
373
+ accessScope: state.accessScope,
374
+ expiresAt: state.expiresAt,
375
+ lastVerifiedAt: state.lastVerifiedAt,
376
+ validationMessage: state.validationMessage,
377
+ };
142
378
  }
143
379
  async verifyLicense(licenseKey) {
144
380
  const trimmedLicenseKey = licenseKey.trim();
@@ -146,18 +382,27 @@ class LicenseCore {
146
382
  throw new LicenseCoreError(exports.LicenseErrorCode.INVALID_LICENSE, '라이선스 키를 입력해 주세요.');
147
383
  }
148
384
  if (this.config.masterCode && trimmedLicenseKey === this.config.masterCode) {
149
- const current = await this.loadState();
385
+ const current = await this.getStatus();
150
386
  const nextState = {
151
387
  ...current,
152
388
  paymentStatus: 'PAID',
389
+ planTier: this.policy.paidTier,
390
+ usageWindowKey: toDateKey(new Date(), this.getUsagePolicy(this.policy.paidTier).reset),
153
391
  licenseKey: trimmedLicenseKey,
392
+ instanceId: 'master-instance',
154
393
  activationLimit: MASTER_CODE_ACTIVATION_LIMIT,
155
394
  activationUsage: 0,
395
+ previousUsageCount: current.usageCount,
156
396
  lastVerifiedAt: new Date().toISOString(),
397
+ licenseStatus: 'active',
398
+ licenseSource: 'master',
399
+ accessScope: 'all-products',
400
+ expiresAt: undefined,
401
+ validationMessage: undefined,
157
402
  };
158
403
  await this.persistState(nextState);
159
404
  await this.notify('마스터 코드로 라이선스가 활성화되었습니다.');
160
- return nextState;
405
+ return this.applyUsageWindowResetIfNeeded(nextState);
161
406
  }
162
407
  let verifyResult;
163
408
  try {
@@ -171,7 +416,7 @@ class LicenseCore {
171
416
  throw mapLicenseError(error, '라이선스 검증 요청에 실패했습니다.');
172
417
  }
173
418
  if (!verifyResult.valid) {
174
- throw mapLicenseError(verifyResult.raw ?? verifyResult.message, '유효하지 않은 라이선스 키입니다.');
419
+ throw mapLicenseError(verifyResult.raw ?? { message: verifyResult.message, errorCode: verifyResult.errorCode }, '유효하지 않은 라이선스 키입니다.');
175
420
  }
176
421
  if (verifyResult.productId
177
422
  && normalizeProductId(verifyResult.productId) !== normalizeProductId(this.config.productId)) {
@@ -182,21 +427,32 @@ class LicenseCore {
182
427
  if (isActivationLimitReached(activationLimit, activationUsage)) {
183
428
  throw new LicenseCoreError(exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED, '활성화 가능한 기기 수를 모두 사용했습니다. 기존 기기를 비활성화한 뒤 다시 시도해 주세요.', verifyResult);
184
429
  }
185
- const current = await this.loadState();
430
+ const current = await this.getStatus();
431
+ const planTier = normalizeTier(verifyResult.planTier || this.policy.paidTier);
432
+ const usageReset = this.getUsagePolicy(planTier).reset;
186
433
  const nextState = {
187
434
  ...current,
188
435
  paymentStatus: 'PAID',
189
- licenseKey: trimmedLicenseKey,
436
+ planTier,
437
+ usageWindowKey: toDateKey(new Date(), usageReset),
438
+ licenseKey: verifyResult.licenseKey || trimmedLicenseKey,
439
+ instanceId: verifyResult.instanceId || '',
190
440
  activationLimit,
191
441
  activationUsage,
442
+ previousUsageCount: current.usageCount,
192
443
  lastVerifiedAt: new Date().toISOString(),
444
+ licenseStatus: verifyResult.licenseStatus || 'active',
445
+ licenseSource: verifyResult.licenseSource,
446
+ accessScope: verifyResult.accessScope,
447
+ expiresAt: verifyResult.expiresAt,
448
+ validationMessage: verifyResult.message || undefined,
193
449
  };
194
450
  await this.persistState(nextState);
195
451
  await this.notify(verifyResult.message || '라이선스가 활성화되었습니다.');
196
- return nextState;
452
+ return this.applyUsageWindowResetIfNeeded(nextState);
197
453
  }
198
454
  async deactivateLicense() {
199
- const current = await this.loadState();
455
+ const current = await this.getStatus();
200
456
  if (!current.licenseKey) {
201
457
  return current;
202
458
  }
@@ -204,6 +460,7 @@ class LicenseCore {
204
460
  licenseKey: current.licenseKey,
205
461
  productId: this.config.productId,
206
462
  instanceName: this.config.instanceName,
463
+ instanceId: current.instanceId,
207
464
  };
208
465
  if (!this.config.masterCode || current.licenseKey !== this.config.masterCode) {
209
466
  try {
@@ -216,24 +473,19 @@ class LicenseCore {
216
473
  throw mapLicenseError(error, '라이선스 비활성화 요청에 실패했습니다.');
217
474
  }
218
475
  }
219
- const nextState = {
220
- ...current,
221
- paymentStatus: 'FREE',
222
- licenseKey: '',
223
- activationLimit: 0,
224
- activationUsage: 0,
225
- lastVerifiedAt: undefined,
226
- };
227
- await this.persistState(nextState);
476
+ const nextState = await this.resetLicenseState();
228
477
  await this.notify('라이선스가 비활성화되었습니다.');
229
- return nextState;
478
+ return this.applyUsageWindowResetIfNeeded(nextState);
230
479
  }
231
480
  async incrementUsage(amount = 1) {
232
- const current = await this.loadState();
481
+ const current = await this.getStatus();
233
482
  if (current.paymentStatus === 'PAID') {
234
483
  return current;
235
484
  }
236
- const increasedAmount = Math.max(0, amount);
485
+ const increasedAmount = Number.isFinite(amount) ? Math.max(0, amount) : 0;
486
+ if (increasedAmount === 0) {
487
+ return current;
488
+ }
237
489
  const nextState = {
238
490
  ...current,
239
491
  usageCount: current.usageCount + increasedAmount,
@@ -241,64 +493,198 @@ class LicenseCore {
241
493
  await this.persistState(nextState);
242
494
  return nextState;
243
495
  }
244
- async canUseFeature() {
245
- const state = await this.loadState();
246
- return this.resolveUsageAccess(state);
496
+ async canUseFeature(featureKey, amount = 1) {
497
+ const state = await this.getStatus();
498
+ return this.resolveUsageAccess(state, featureKey, amount);
247
499
  }
248
- resolveUsageAccess(state) {
249
- if (state.paymentStatus === 'PAID') {
500
+ async consumeUsage(amount = 1, featureKey) {
501
+ const execute = async () => {
502
+ const state = await this.getStatus();
503
+ const requestedAmount = Number.isFinite(amount) ? Math.max(0, amount) : 0;
504
+ const access = this.resolveUsageAccess(state, featureKey, requestedAmount);
505
+ if (!access.allowed || requestedAmount <= 0) {
506
+ return { state, access, consumed: false };
507
+ }
508
+ const updatedState = await this.incrementUsage(requestedAmount);
509
+ const updatedAccess = this.resolveUsageAccess(updatedState, featureKey, requestedAmount);
250
510
  return {
251
- allowed: true,
511
+ state: updatedState,
512
+ access: updatedAccess,
513
+ consumed: true,
514
+ };
515
+ };
516
+ // Figma can deliver duplicate UI events while a panel remounts. Serialize
517
+ // the check + persist pair so two concurrent requests cannot overspend the
518
+ // same local allowance.
519
+ const pending = this.usageMutation.then(execute, execute);
520
+ this.usageMutation = pending.then(() => undefined, () => undefined);
521
+ return pending;
522
+ }
523
+ resolveUsageAccess(state, featureKey, amount = 1) {
524
+ const tier = this.resolveTier(state);
525
+ const usagePolicy = this.getUsagePolicy(tier);
526
+ const featureAllowed = this.isFeatureAllowed(tier, featureKey);
527
+ const isLicensed = this.isLicenseRegisteredState(state);
528
+ const requested = Number.isFinite(amount) && amount > 0 ? amount : 1;
529
+ const used = state.usageCount;
530
+ if (usagePolicy.limit === 'unlimited') {
531
+ return {
532
+ allowed: featureAllowed,
252
533
  remaining: Number.POSITIVE_INFINITY,
534
+ used,
535
+ limit: 'unlimited',
536
+ reset: usagePolicy.reset,
537
+ requested,
538
+ tier,
539
+ hasLicenseKey: Boolean(state.licenseKey),
540
+ isLicenseRegistered: isLicensed,
541
+ featureAllowed,
542
+ featureKey,
543
+ reason: featureAllowed ? undefined : 'FEATURE_NOT_ALLOWED',
253
544
  };
254
545
  }
255
- const remaining = Math.max(this.usageLimit - state.usageCount, 0);
546
+ const remaining = Math.max(usagePolicy.limit - used, 0);
547
+ const usageAllowed = remaining >= requested;
548
+ const allowed = featureAllowed && usageAllowed;
256
549
  return {
257
- allowed: remaining > 0,
550
+ allowed,
258
551
  remaining,
552
+ used,
553
+ limit: usagePolicy.limit,
554
+ reset: usagePolicy.reset,
555
+ requested,
556
+ tier,
557
+ hasLicenseKey: Boolean(state.licenseKey),
558
+ isLicenseRegistered: isLicensed,
559
+ featureAllowed,
560
+ featureKey,
561
+ reason: featureAllowed ? (usageAllowed ? undefined : 'USAGE_LIMIT_REACHED') : 'FEATURE_NOT_ALLOWED',
259
562
  };
260
563
  }
261
- getUsageLimit() {
262
- return this.usageLimit;
564
+ getUsageLimit(state) {
565
+ const tier = state ? this.resolveTier(state) : this.policy.freeTier;
566
+ return this.getUsagePolicy(tier).limit;
263
567
  }
264
568
  getStoragePrefix() {
265
569
  return this.storagePrefix;
266
570
  }
571
+ resolveTier(state) {
572
+ const storedTier = normalizeTier(state.planTier || '');
573
+ {
574
+ return storedTier;
575
+ }
576
+ }
577
+ getUsagePolicy(tier) {
578
+ return this.policy.usageByTier[tier]
579
+ || this.policy.usageByTier[this.policy.defaultTier]
580
+ || { limit: 'unlimited', reset: 'none' };
581
+ }
582
+ isFeatureAllowed(tier, featureKey) {
583
+ if (!featureKey) {
584
+ return true;
585
+ }
586
+ const policy = this.policy.featuresByTier[tier];
587
+ if (!policy) {
588
+ return true;
589
+ }
590
+ if (policy.deny?.includes(featureKey)) {
591
+ return false;
592
+ }
593
+ if (policy.allow && policy.allow.length > 0) {
594
+ return policy.allow.includes(featureKey);
595
+ }
596
+ return policy.defaultAllowed ?? true;
597
+ }
598
+ isLicenseRegisteredState(state) {
599
+ const status = state.licenseStatus?.toLowerCase();
600
+ const invalidStatuses = ['invalid', 'inactive', 'expired', 'revoked', 'unpaid'];
601
+ return state.paymentStatus === 'PAID'
602
+ && Boolean(state.licenseKey)
603
+ && !invalidStatuses.includes(status || '');
604
+ }
267
605
  storageKey(name) {
268
606
  return `${this.storagePrefix}:${STORAGE_KEYS[name]}`;
269
607
  }
270
608
  async loadState() {
271
609
  try {
272
- const [paymentStatus, licenseKey, activationLimit, activationUsage, usageCount, lastVerifiedAt,] = await Promise.all([
610
+ const [paymentStatus, licenseKey, instanceId, activationLimit, activationUsage, usageCount, previousUsageCount, usageWindowKey, planTier, lastVerifiedAt, licenseStatus, licenseSource, accessScope, expiresAt, validationMessage,] = await Promise.all([
273
611
  this.deps.storage.get(this.storageKey('paymentStatus')),
274
612
  this.deps.storage.get(this.storageKey('licenseKey')),
613
+ this.deps.storage.get(this.storageKey('instanceId')),
275
614
  this.deps.storage.get(this.storageKey('activationLimit')),
276
615
  this.deps.storage.get(this.storageKey('activationUsage')),
277
616
  this.deps.storage.get(this.storageKey('usageCount')),
617
+ this.deps.storage.get(this.storageKey('previousUsageCount')),
618
+ this.deps.storage.get(this.storageKey('usageWindowKey')),
619
+ this.deps.storage.get(this.storageKey('planTier')),
278
620
  this.deps.storage.get(this.storageKey('lastVerifiedAt')),
621
+ this.deps.storage.get(this.storageKey('licenseStatus')),
622
+ this.deps.storage.get(this.storageKey('licenseSource')),
623
+ this.deps.storage.get(this.storageKey('accessScope')),
624
+ this.deps.storage.get(this.storageKey('expiresAt')),
625
+ this.deps.storage.get(this.storageKey('validationMessage')),
279
626
  ]);
627
+ const normalizedPaymentStatus = toPaymentStatus(paymentStatus);
628
+ const resolvedTier = normalizeTier(toText(planTier)
629
+ || (normalizedPaymentStatus === 'PAID' ? this.policy.paidTier : this.policy.freeTier));
630
+ const usageReset = this.getUsagePolicy(resolvedTier).reset;
280
631
  return {
281
- paymentStatus: toPaymentStatus(paymentStatus),
632
+ paymentStatus: normalizedPaymentStatus,
282
633
  licenseKey: toText(licenseKey),
634
+ instanceId: toText(instanceId),
283
635
  activationLimit: toNonNegativeNumber(activationLimit, 0),
284
636
  activationUsage: toNonNegativeNumber(activationUsage, 0),
285
637
  usageCount: toNonNegativeNumber(usageCount, 0),
638
+ previousUsageCount: toNonNegativeNumber(previousUsageCount, 0),
639
+ usageWindowKey: toText(usageWindowKey) || toDateKey(new Date(), usageReset),
640
+ planTier: resolvedTier,
286
641
  lastVerifiedAt: toText(lastVerifiedAt) || undefined,
642
+ licenseStatus: toText(licenseStatus) || undefined,
643
+ licenseSource: toText(licenseSource) || undefined,
644
+ accessScope: toText(accessScope) || undefined,
645
+ expiresAt: toText(expiresAt) || undefined,
646
+ validationMessage: toText(validationMessage) || undefined,
287
647
  };
288
648
  }
289
649
  catch (error) {
290
650
  throw new LicenseCoreError(exports.LicenseErrorCode.STORAGE_ERROR, '라이선스 상태를 불러오지 못했습니다.', error, true);
291
651
  }
292
652
  }
653
+ async applyUsageWindowResetIfNeeded(state) {
654
+ const tier = this.resolveTier(state);
655
+ const usageReset = this.getUsagePolicy(tier).reset;
656
+ const expectedWindowKey = toDateKey(new Date(), usageReset);
657
+ const requiresUsageReset = usageReset !== 'none' && state.usageWindowKey !== expectedWindowKey;
658
+ const requiresWindowMigration = state.usageWindowKey !== expectedWindowKey;
659
+ if (!requiresUsageReset && !requiresWindowMigration) {
660
+ return state;
661
+ }
662
+ const nextState = {
663
+ ...state,
664
+ usageWindowKey: expectedWindowKey,
665
+ usageCount: requiresUsageReset ? 0 : state.usageCount,
666
+ };
667
+ await this.persistState(nextState);
668
+ return nextState;
669
+ }
293
670
  async persistState(state) {
294
671
  try {
295
672
  await Promise.all([
296
673
  this.deps.storage.set(this.storageKey('paymentStatus'), state.paymentStatus),
297
674
  this.deps.storage.set(this.storageKey('licenseKey'), state.licenseKey),
675
+ this.deps.storage.set(this.storageKey('instanceId'), state.instanceId || ''),
298
676
  this.deps.storage.set(this.storageKey('activationLimit'), state.activationLimit),
299
677
  this.deps.storage.set(this.storageKey('activationUsage'), state.activationUsage),
300
678
  this.deps.storage.set(this.storageKey('usageCount'), state.usageCount),
679
+ this.deps.storage.set(this.storageKey('previousUsageCount'), state.previousUsageCount || 0),
680
+ this.deps.storage.set(this.storageKey('usageWindowKey'), state.usageWindowKey),
681
+ this.deps.storage.set(this.storageKey('planTier'), state.planTier),
301
682
  this.deps.storage.set(this.storageKey('lastVerifiedAt'), state.lastVerifiedAt || ''),
683
+ this.deps.storage.set(this.storageKey('licenseStatus'), state.licenseStatus || ''),
684
+ this.deps.storage.set(this.storageKey('licenseSource'), state.licenseSource || ''),
685
+ this.deps.storage.set(this.storageKey('accessScope'), state.accessScope || ''),
686
+ this.deps.storage.set(this.storageKey('expiresAt'), state.expiresAt || ''),
687
+ this.deps.storage.set(this.storageKey('validationMessage'), state.validationMessage || ''),
302
688
  ]);
303
689
  }
304
690
  catch (error) {
@@ -348,18 +734,47 @@ function readNumberField(raw, keys) {
348
734
  }
349
735
  return undefined;
350
736
  }
351
- function asRecord(value) {
737
+ function readBooleanField(raw, keys) {
738
+ if (!raw || typeof raw !== 'object') {
739
+ return undefined;
740
+ }
741
+ for (const key of keys) {
742
+ const value = raw[key];
743
+ if (typeof value === 'boolean') {
744
+ return value;
745
+ }
746
+ if (typeof value === 'string') {
747
+ const normalized = value.trim().toLowerCase();
748
+ if (['true', '1', 'yes', 'active', 'valid', 'success'].includes(normalized)) {
749
+ return true;
750
+ }
751
+ if (['false', '0', 'no', 'inactive', 'invalid', 'revoked', 'expired', 'error'].includes(normalized)) {
752
+ return false;
753
+ }
754
+ }
755
+ }
756
+ return undefined;
757
+ }
758
+ function asRecord$1(value) {
352
759
  return value && typeof value === 'object' ? value : {};
353
760
  }
354
761
  function defaultNormalizeVerifyResponse(raw) {
355
- const body = asRecord(raw);
356
- const valid = Boolean(body.valid
357
- ?? body.success
358
- ?? body.active
359
- ?? (typeof body.status === 'string' && body.status.toLowerCase() === 'active'));
762
+ const body = asRecord$1(raw);
763
+ const licenseStatus = readTextField(body, ['licenseStatus', 'license_status', 'status'])
764
+ || readTextField(asRecord$1(body.meta), ['license_status', 'status']);
765
+ const statusValid = typeof licenseStatus === 'string'
766
+ ? licenseStatus.toLowerCase() === 'active'
767
+ : undefined;
768
+ const valid = readBooleanField(body, ['valid', 'success', 'active'])
769
+ ?? statusValid
770
+ ?? false;
360
771
  const message = readTextField(body, ['message', 'error', 'detail']);
361
- const nestedLicenseKey = asRecord(body.license_key);
362
- const nestedMeta = asRecord(body.meta);
772
+ const nestedLicenseKey = asRecord$1(body.license_key);
773
+ const nestedMeta = asRecord$1(body.meta);
774
+ const nestedInstance = asRecord$1(body.instance);
775
+ const instanceId = readTextField(body, ['instanceId', 'instance_id'])
776
+ || readTextField(nestedInstance, ['id'])
777
+ || readTextField(nestedLicenseKey, ['instance_id']);
363
778
  const productId = readTextField(body, ['productId', 'product_id'])
364
779
  || readTextField(nestedMeta, ['product_id'])
365
780
  || readTextField(nestedLicenseKey, ['product_id']);
@@ -369,17 +784,37 @@ function defaultNormalizeVerifyResponse(raw) {
369
784
  const activationUsage = readNumberField(body, ['activationUsage', 'activation_usage'])
370
785
  ?? readNumberField(nestedMeta, ['activation_usage'])
371
786
  ?? readNumberField(nestedLicenseKey, ['activation_usage']);
787
+ const planTier = readTextField(body, ['planTier', 'tier', 'plan'])
788
+ || readTextField(nestedMeta, ['tier', 'plan']);
789
+ const licenseSource = readTextField(body, ['licenseSource', 'license_source', 'source'])
790
+ || readTextField(nestedMeta, ['license_source', 'source'])
791
+ || readTextField(nestedLicenseKey, ['license_source', 'source']);
792
+ const accessScope = readTextField(body, ['accessScope', 'access_scope', 'scope'])
793
+ || readTextField(nestedMeta, ['access_scope', 'scope'])
794
+ || readTextField(nestedLicenseKey, ['access_scope', 'scope']);
795
+ const expiresAt = readTextField(body, ['expiresAt', 'expires_at', 'expiration', 'expiration_date'])
796
+ || readTextField(nestedMeta, ['expires_at', 'expiration', 'expiration_date'])
797
+ || readTextField(nestedLicenseKey, ['expires_at', 'expiration', 'expiration_date']);
798
+ const errorCode = readTextField(body, ['errorCode', 'error_code', 'code'])
799
+ || readTextField(nestedMeta, ['error_code', 'code']);
372
800
  return {
373
801
  valid,
374
802
  message,
375
803
  productId,
376
804
  activationLimit,
377
805
  activationUsage,
806
+ instanceId,
807
+ planTier,
808
+ licenseStatus,
809
+ licenseSource,
810
+ accessScope,
811
+ expiresAt,
812
+ errorCode,
378
813
  raw,
379
814
  };
380
815
  }
381
816
  function defaultNormalizeDeactivateResponse(raw) {
382
- const body = asRecord(raw);
817
+ const body = asRecord$1(raw);
383
818
  const success = Boolean(body.success
384
819
  ?? body.deactivated
385
820
  ?? body.valid
@@ -390,6 +825,14 @@ function defaultNormalizeDeactivateResponse(raw) {
390
825
  raw,
391
826
  };
392
827
  }
828
+ class LicenseTransportHttpError extends Error {
829
+ constructor(status, body, message) {
830
+ super(message);
831
+ this.status = status;
832
+ this.body = body;
833
+ this.name = 'LicenseTransportHttpError';
834
+ }
835
+ }
393
836
  class FetchLicenseTransport {
394
837
  constructor(config) {
395
838
  this.config = config;
@@ -403,6 +846,15 @@ class FetchLicenseTransport {
403
846
  const normalize = this.config.normalizeVerifyResponse ?? defaultNormalizeVerifyResponse;
404
847
  return normalize(raw);
405
848
  }
849
+ async validate(payload) {
850
+ const url = this.config.validateUrl || this.config.verifyUrl;
851
+ const body = this.config.buildValidateBody
852
+ ? this.config.buildValidateBody(payload)
853
+ : payload;
854
+ const raw = await this.postJson(url, body);
855
+ const normalize = this.config.normalizeVerifyResponse ?? defaultNormalizeVerifyResponse;
856
+ return normalize(raw);
857
+ }
406
858
  async deactivate(payload) {
407
859
  const url = this.config.deactivateUrl || this.config.verifyUrl;
408
860
  const body = this.config.buildDeactivateBody
@@ -432,8 +884,143 @@ class FetchLicenseTransport {
432
884
  signal: abortController?.signal,
433
885
  });
434
886
  if (!response.ok) {
435
- const errorText = await response.text();
436
- throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`);
887
+ let errorBody;
888
+ try {
889
+ errorBody = await response.json();
890
+ }
891
+ catch {
892
+ errorBody = await response.text();
893
+ }
894
+ const errorRecord = asRecord$1(errorBody);
895
+ const errorMessage = readTextField(errorRecord, ['message', 'error', 'detail'])
896
+ || (typeof errorBody === 'string' ? errorBody : '')
897
+ || response.statusText;
898
+ throw new LicenseTransportHttpError(response.status, errorBody, `HTTP ${response.status}: ${errorMessage}`);
899
+ }
900
+ return await response.json();
901
+ }
902
+ finally {
903
+ if (timeoutId) {
904
+ clearTimeout(timeoutId);
905
+ }
906
+ }
907
+ }
908
+ }
909
+
910
+ function asRecord(value) {
911
+ return value && typeof value === 'object' ? value : {};
912
+ }
913
+ function readText(raw, keys) {
914
+ const body = asRecord(raw);
915
+ for (const key of keys) {
916
+ const value = body[key];
917
+ if (typeof value === 'string' && value.trim() !== '') {
918
+ return value;
919
+ }
920
+ }
921
+ return '';
922
+ }
923
+ function readNumber(raw, keys) {
924
+ const body = asRecord(raw);
925
+ for (const key of keys) {
926
+ const value = body[key];
927
+ if (typeof value === 'number' && Number.isFinite(value)) {
928
+ return value;
929
+ }
930
+ if (typeof value === 'string' && value.trim() !== '') {
931
+ const numeric = Number(value);
932
+ if (Number.isFinite(numeric)) {
933
+ return numeric;
934
+ }
935
+ }
936
+ }
937
+ return undefined;
938
+ }
939
+ function joinUrl(baseUrl, path) {
940
+ return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
941
+ }
942
+ class DesignbaseAccountAuthClient {
943
+ constructor(config) {
944
+ this.config = config;
945
+ if (!config.baseUrl.trim()) {
946
+ throw new Error('DesignbaseAccountAuthConfig.baseUrl is required.');
947
+ }
948
+ this.fetchFn = config.fetchFn ?? fetch;
949
+ }
950
+ async start(payload) {
951
+ const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.startPath || '/wp-json/custom/v1/figma/auth/start'), {
952
+ plugin_code: payload.pluginCode,
953
+ device_id: payload.deviceId,
954
+ return_url: payload.returnUrl || '',
955
+ });
956
+ return {
957
+ sessionId: readText(raw, ['session_id', 'sessionId']),
958
+ pollToken: readText(raw, ['poll_token', 'pollToken']),
959
+ authUrl: readText(raw, ['auth_url', 'authUrl']),
960
+ expiresIn: readNumber(raw, ['expires_in', 'expiresIn']) ?? 600,
961
+ pollInterval: readNumber(raw, ['poll_interval', 'pollInterval']) ?? 3,
962
+ raw,
963
+ };
964
+ }
965
+ async poll(sessionId, pollToken) {
966
+ const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.pollPath || '/wp-json/custom/v1/figma/auth/poll'), {
967
+ session_id: sessionId,
968
+ poll_token: pollToken,
969
+ });
970
+ const status = readText(raw, ['status']) || 'pending';
971
+ const allowed = Boolean(asRecord(raw).allowed || status === 'active');
972
+ const tokens = asRecord(asRecord(raw).tokens);
973
+ const actions = asRecord(asRecord(raw).actions);
974
+ return {
975
+ status,
976
+ allowed,
977
+ pluginCode: readText(raw, ['plugin_code', 'pluginCode']),
978
+ productId: readText(raw, ['product_id', 'productId']) || undefined,
979
+ planTier: readText(raw, ['plan_tier', 'planTier']) || undefined,
980
+ licenseKey: readText(raw, ['license_key', 'licenseKey']) || undefined,
981
+ accessToken: readText(tokens, ['access_token', 'accessToken']) || undefined,
982
+ refreshToken: readText(tokens, ['refresh_token', 'refreshToken']) || undefined,
983
+ expiresIn: readNumber(tokens, ['expires_in', 'expiresIn']),
984
+ message: readText(raw, ['message']) || undefined,
985
+ actions,
986
+ raw,
987
+ };
988
+ }
989
+ async saveTokens(result) {
990
+ if (!this.config.storage || !result.accessToken) {
991
+ return;
992
+ }
993
+ const prefix = this.config.storagePrefix || 'designbase-account-auth';
994
+ await Promise.all([
995
+ this.config.storage.set(`${prefix}:accessToken`, result.accessToken),
996
+ this.config.storage.set(`${prefix}:refreshToken`, result.refreshToken || ''),
997
+ this.config.storage.set(`${prefix}:expiresAt`, Date.now() + ((result.expiresIn || 1800) * 1000)),
998
+ ]);
999
+ }
1000
+ async getAccessToken() {
1001
+ if (!this.config.storage) {
1002
+ return '';
1003
+ }
1004
+ const prefix = this.config.storagePrefix || 'designbase-account-auth';
1005
+ const token = await this.config.storage.get(`${prefix}:accessToken`);
1006
+ return typeof token === 'string' ? token : '';
1007
+ }
1008
+ async postJson(url, body) {
1009
+ const timeoutMs = this.config.timeoutMs ?? 15000;
1010
+ const abortController = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
1011
+ const timeoutId = abortController ? setTimeout(() => abortController.abort(), timeoutMs) : undefined;
1012
+ try {
1013
+ const response = await this.fetchFn(url, {
1014
+ method: 'POST',
1015
+ headers: {
1016
+ 'Content-Type': 'application/json',
1017
+ },
1018
+ body: JSON.stringify(body),
1019
+ signal: abortController?.signal,
1020
+ });
1021
+ if (!response.ok) {
1022
+ const text = await response.text();
1023
+ throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
437
1024
  }
438
1025
  return await response.json();
439
1026
  }
@@ -444,6 +1031,94 @@ class FetchLicenseTransport {
444
1031
  }
445
1032
  }
446
1033
  }
1034
+ class DesignbaseAccountLicenseTransport {
1035
+ constructor(config) {
1036
+ this.config = config;
1037
+ this.client = new DesignbaseAccountAuthClient(config);
1038
+ this.fetchFn = config.fetchFn ?? fetch;
1039
+ }
1040
+ async verify(payload) {
1041
+ return this.validate(payload);
1042
+ }
1043
+ async validate(payload) {
1044
+ const token = await this.resolveAccessToken();
1045
+ if (!token) {
1046
+ return {
1047
+ valid: false,
1048
+ message: 'Designbase account access token is missing.',
1049
+ raw: { status: 'not_logged_in' },
1050
+ };
1051
+ }
1052
+ const url = new URL(joinUrl(this.config.baseUrl, this.config.statusPath || '/wp-json/custom/v1/product/status'));
1053
+ url.searchParams.set('plugin_code', payload.productId);
1054
+ const raw = await this.getJson(url.toString(), token);
1055
+ const status = readText(raw, ['status']);
1056
+ const allowed = Boolean(asRecord(raw).allowed || status === 'active');
1057
+ return {
1058
+ valid: allowed,
1059
+ message: readText(raw, ['message']) || undefined,
1060
+ productId: readText(raw, ['plugin_code', 'pluginCode']) || payload.productId,
1061
+ instanceId: payload.instanceId,
1062
+ activationLimit: readNumber(raw, ['activation_limit', 'activationLimit']) ?? 999999,
1063
+ activationUsage: readNumber(raw, ['activation_usage', 'activationUsage']) ?? 0,
1064
+ planTier: readText(raw, ['plan_tier', 'planTier']) || (allowed ? 'PRO' : 'FREE'),
1065
+ licenseKey: payload.licenseKey,
1066
+ raw,
1067
+ };
1068
+ }
1069
+ async deactivate(payload) {
1070
+ const token = await this.resolveAccessToken();
1071
+ if (!token) {
1072
+ return { success: true, message: 'Already signed out.' };
1073
+ }
1074
+ const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.deactivatePath || '/wp-json/custom/v1/product/deactivate'), {
1075
+ license_key: payload.licenseKey,
1076
+ device_id: payload.instanceId || payload.instanceName,
1077
+ plugin_code: payload.productId,
1078
+ }, token);
1079
+ const status = readText(raw, ['status']);
1080
+ return {
1081
+ success: Boolean(asRecord(raw).success || status === 'ok' || status === 'inactive'),
1082
+ message: readText(raw, ['message']) || undefined,
1083
+ raw,
1084
+ };
1085
+ }
1086
+ async resolveAccessToken() {
1087
+ if (this.config.getAccessToken) {
1088
+ const token = await this.config.getAccessToken();
1089
+ return typeof token === 'string' ? token : '';
1090
+ }
1091
+ return this.client.getAccessToken();
1092
+ }
1093
+ async getJson(url, token) {
1094
+ const response = await this.fetchFn(url, {
1095
+ method: 'GET',
1096
+ headers: {
1097
+ Authorization: `Bearer ${token}`,
1098
+ },
1099
+ });
1100
+ if (!response.ok) {
1101
+ const text = await response.text();
1102
+ throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
1103
+ }
1104
+ return response.json();
1105
+ }
1106
+ async postJson(url, body, token) {
1107
+ const response = await this.fetchFn(url, {
1108
+ method: 'POST',
1109
+ headers: {
1110
+ Authorization: `Bearer ${token}`,
1111
+ 'Content-Type': 'application/json',
1112
+ },
1113
+ body: JSON.stringify(body),
1114
+ });
1115
+ if (!response.ok) {
1116
+ const text = await response.text();
1117
+ throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
1118
+ }
1119
+ return response.json();
1120
+ }
1121
+ }
447
1122
 
448
1123
  class FigmaStorageAdapter {
449
1124
  constructor(figma) {
@@ -474,25 +1149,38 @@ class FigmaNotifierAdapter {
474
1149
  }
475
1150
  }
476
1151
  }
477
- const DEFAULT_MESSAGE_TYPES = {
1152
+ const DEFAULT_LICENSE_MESSAGE_TYPES = {
478
1153
  initialize: 'initialize',
479
1154
  verifyLicense: 'verify-license',
480
1155
  deactivateLicense: 'deactivate-license',
481
1156
  updateStatus: 'update-plugin-status',
482
1157
  verifyResult: 'license-verification-result',
483
1158
  deactivateResult: 'license-deactivation-result',
1159
+ checkFeatureAccess: 'check-feature-access',
1160
+ consumeUsage: 'consume-usage',
1161
+ featureAccessResult: 'feature-access-result',
1162
+ usageConsumeResult: 'usage-consume-result',
484
1163
  };
485
1164
  class FigmaLicenseStatusManager {
486
1165
  constructor(options) {
487
1166
  this.options = options;
488
1167
  this.messageTypes = {
489
- ...DEFAULT_MESSAGE_TYPES,
1168
+ ...DEFAULT_LICENSE_MESSAGE_TYPES,
490
1169
  ...(options.messageTypes || {}),
491
1170
  };
492
1171
  this.notifier = options.notifier;
493
1172
  }
494
1173
  async initialize() {
495
- await this.postStatusUpdate();
1174
+ if (!this.initializationPromise) {
1175
+ this.initializationPromise = (async () => {
1176
+ const state = await this.options.core.initialize();
1177
+ await this.postStatusUpdate(state);
1178
+ })().catch((error) => {
1179
+ this.initializationPromise = undefined;
1180
+ throw error;
1181
+ });
1182
+ }
1183
+ await this.initializationPromise;
496
1184
  }
497
1185
  async handleMessage(pluginMessage) {
498
1186
  if (!pluginMessage || typeof pluginMessage !== 'object') {
@@ -504,7 +1192,7 @@ class FigmaLicenseStatusManager {
504
1192
  return false;
505
1193
  }
506
1194
  if (type === this.messageTypes.initialize) {
507
- await this.postStatusUpdate();
1195
+ await this.initialize();
508
1196
  return true;
509
1197
  }
510
1198
  if (type === this.messageTypes.verifyLicense) {
@@ -516,6 +1204,22 @@ class FigmaLicenseStatusManager {
516
1204
  await this.handleDeactivateLicense();
517
1205
  return true;
518
1206
  }
1207
+ if (type === this.messageTypes.checkFeatureAccess) {
1208
+ const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
1209
+ const amount = typeof message.amount === 'number' && Number.isFinite(message.amount) && message.amount > 0
1210
+ ? message.amount
1211
+ : 1;
1212
+ await this.handleCheckFeatureAccess(featureKey, amount);
1213
+ return true;
1214
+ }
1215
+ if (type === this.messageTypes.consumeUsage) {
1216
+ const amount = typeof message.amount === 'number' && Number.isFinite(message.amount) && message.amount > 0
1217
+ ? message.amount
1218
+ : 1;
1219
+ const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
1220
+ await this.handleConsumeUsage(amount, featureKey);
1221
+ return true;
1222
+ }
519
1223
  return false;
520
1224
  }
521
1225
  bindToOnMessage(fallbackHandler) {
@@ -527,18 +1231,40 @@ class FigmaLicenseStatusManager {
527
1231
  }
528
1232
  };
529
1233
  }
1234
+ /**
1235
+ * Check a feature from main code using the same policy as the UI messages.
1236
+ * This is the preferred guard for operations handled entirely in main.
1237
+ */
1238
+ async canUseFeature(featureKey, amount = 1) {
1239
+ const state = await this.options.core.getStatus();
1240
+ const safeAmount = Number.isFinite(amount) && amount > 0 ? amount : 1;
1241
+ return this.options.core.resolveUsageAccess(state, featureKey, safeAmount);
1242
+ }
1243
+ /**
1244
+ * Consume usage from main code and publish the refreshed status to the UI.
1245
+ */
1246
+ async consumeUsage(amount = 1, featureKey) {
1247
+ const result = await this.options.core.consumeUsage(amount, featureKey);
1248
+ await this.postStatusUpdate(result.state);
1249
+ return result;
1250
+ }
530
1251
  async handleVerifyLicense(licenseKey) {
531
1252
  try {
532
1253
  const state = await this.options.core.verifyLicense(licenseKey);
533
1254
  const access = this.options.core.resolveUsageAccess(state);
1255
+ const summary = await this.options.core.getLicenseSummary();
534
1256
  this.postMessage({
535
1257
  type: this.messageTypes.verifyResult,
536
1258
  success: true,
537
1259
  message: '라이선스가 검증되었습니다.',
538
1260
  ...state,
539
- usageLimit: this.options.core.getUsageLimit(),
1261
+ ...summary,
1262
+ usageLimit: this.options.core.getUsageLimit(state),
540
1263
  canUseFeature: access.allowed,
541
1264
  remainingUsage: access.remaining,
1265
+ usageUsed: access.used,
1266
+ usageReset: access.reset,
1267
+ usageTier: access.tier,
542
1268
  });
543
1269
  await this.postStatusUpdate(state);
544
1270
  }
@@ -551,20 +1277,26 @@ class FigmaLicenseStatusManager {
551
1277
  message: mappedError.message,
552
1278
  errorCode: mappedError.code,
553
1279
  });
1280
+ await this.postStatusUpdate();
554
1281
  }
555
1282
  }
556
1283
  async handleDeactivateLicense() {
557
1284
  try {
558
1285
  const state = await this.options.core.deactivateLicense();
559
1286
  const access = this.options.core.resolveUsageAccess(state);
1287
+ const summary = await this.options.core.getLicenseSummary();
560
1288
  this.postMessage({
561
1289
  type: this.messageTypes.deactivateResult,
562
1290
  success: true,
563
1291
  message: '라이선스가 비활성화되었습니다.',
564
1292
  ...state,
565
- usageLimit: this.options.core.getUsageLimit(),
1293
+ ...summary,
1294
+ usageLimit: this.options.core.getUsageLimit(state),
566
1295
  canUseFeature: access.allowed,
567
1296
  remainingUsage: access.remaining,
1297
+ usageUsed: access.used,
1298
+ usageReset: access.reset,
1299
+ usageTier: access.tier,
568
1300
  });
569
1301
  await this.postStatusUpdate(state);
570
1302
  }
@@ -582,13 +1314,47 @@ class FigmaLicenseStatusManager {
582
1314
  async postStatusUpdate(initialState) {
583
1315
  const state = initialState ?? await this.options.core.getStatus();
584
1316
  const access = this.options.core.resolveUsageAccess(state);
1317
+ const summary = await this.options.core.getLicenseSummary();
585
1318
  this.postMessage({
586
1319
  type: this.messageTypes.updateStatus,
587
1320
  ...state,
588
- usageLimit: this.options.core.getUsageLimit(),
1321
+ ...summary,
1322
+ usageLimit: this.options.core.getUsageLimit(state),
589
1323
  canUseFeature: access.allowed,
590
1324
  remainingUsage: access.remaining,
1325
+ usageUsed: access.used,
1326
+ usageReset: access.reset,
1327
+ usageTier: access.tier,
1328
+ usageReason: access.reason,
1329
+ });
1330
+ }
1331
+ async handleCheckFeatureAccess(featureKey, amount = 1) {
1332
+ const state = await this.options.core.getStatus();
1333
+ const access = this.options.core.resolveUsageAccess(state, featureKey, amount);
1334
+ const summary = await this.options.core.getLicenseSummary();
1335
+ this.postMessage({
1336
+ type: this.messageTypes.featureAccessResult,
1337
+ success: true,
1338
+ featureKey,
1339
+ amount,
1340
+ ...summary,
1341
+ ...access,
1342
+ });
1343
+ }
1344
+ async handleConsumeUsage(amount, featureKey) {
1345
+ const result = await this.options.core.consumeUsage(amount, featureKey);
1346
+ const summary = await this.options.core.getLicenseSummary();
1347
+ this.postMessage({
1348
+ type: this.messageTypes.usageConsumeResult,
1349
+ success: result.consumed,
1350
+ featureKey,
1351
+ amount,
1352
+ consumed: result.consumed,
1353
+ ...summary,
1354
+ ...result.access,
1355
+ usageCount: result.state.usageCount,
591
1356
  });
1357
+ await this.postStatusUpdate(result.state);
592
1358
  }
593
1359
  postMessage(message) {
594
1360
  this.options.figma.ui.postMessage(message);
@@ -616,6 +1382,7 @@ function createFigmaLicenseStatusManager(options) {
616
1382
  const notifier = options.notifier ?? new FigmaNotifierAdapter(figmaApi);
617
1383
  const transport = options.transport ?? new FetchLicenseTransport({
618
1384
  verifyUrl: options.config.endpoint,
1385
+ validateUrl: options.config.validateEndpoint,
619
1386
  deactivateUrl: options.config.deactivateEndpoint,
620
1387
  headers: options.config.requestHeaders,
621
1388
  timeoutMs: options.config.requestTimeoutMs,
@@ -635,12 +1402,16 @@ function createFigmaLicenseStatusManager(options) {
635
1402
  });
636
1403
  }
637
1404
 
1405
+ exports.DEFAULT_LICENSE_MESSAGE_TYPES = DEFAULT_LICENSE_MESSAGE_TYPES;
1406
+ exports.DesignbaseAccountAuthClient = DesignbaseAccountAuthClient;
1407
+ exports.DesignbaseAccountLicenseTransport = DesignbaseAccountLicenseTransport;
638
1408
  exports.FetchLicenseTransport = FetchLicenseTransport;
639
1409
  exports.FigmaLicenseStatusManager = FigmaLicenseStatusManager;
640
1410
  exports.FigmaNotifierAdapter = FigmaNotifierAdapter;
641
1411
  exports.FigmaStorageAdapter = FigmaStorageAdapter;
642
1412
  exports.LicenseCore = LicenseCore;
643
1413
  exports.LicenseCoreError = LicenseCoreError;
1414
+ exports.LicenseTransportHttpError = LicenseTransportHttpError;
644
1415
  exports.createFigmaLicenseStatusManager = createFigmaLicenseStatusManager;
645
1416
  exports.defaultNormalizeDeactivateResponse = defaultNormalizeDeactivateResponse;
646
1417
  exports.defaultNormalizeVerifyResponse = defaultNormalizeVerifyResponse;