@designbasekorea/figma-license-core 0.1.0 → 0.2.1

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.esm.js CHANGED
@@ -2,6 +2,9 @@ var LicenseErrorCode;
2
2
  (function (LicenseErrorCode) {
3
3
  LicenseErrorCode["ACTIVATION_LIMIT_REACHED"] = "ACTIVATION_LIMIT_REACHED";
4
4
  LicenseErrorCode["INVALID_LICENSE"] = "INVALID_LICENSE";
5
+ LicenseErrorCode["LICENSE_EXPIRED"] = "LICENSE_EXPIRED";
6
+ LicenseErrorCode["LICENSE_REVOKED"] = "LICENSE_REVOKED";
7
+ LicenseErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
5
8
  LicenseErrorCode["PRODUCT_MISMATCH"] = "PRODUCT_MISMATCH";
6
9
  LicenseErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
7
10
  LicenseErrorCode["STORAGE_ERROR"] = "STORAGE_ERROR";
@@ -25,10 +28,18 @@ function asText(input) {
25
28
  if (typeof input === 'string') {
26
29
  return input;
27
30
  }
28
- if (typeof input === 'object' && 'message' in input) {
29
- const maybeMessage = input.message;
30
- if (typeof maybeMessage === 'string') {
31
- return maybeMessage;
31
+ if (typeof input === 'object') {
32
+ const objectInput = input;
33
+ if (objectInput.raw) {
34
+ return asText(objectInput.raw);
35
+ }
36
+ if (objectInput.body) {
37
+ return asText(objectInput.body);
38
+ }
39
+ const fields = [objectInput.message, objectInput.errorCode, objectInput.code, objectInput.status]
40
+ .filter((value) => typeof value === 'string' && value.trim() !== '');
41
+ if (fields.length > 0) {
42
+ return fields.join(' ');
32
43
  }
33
44
  }
34
45
  try {
@@ -46,13 +57,32 @@ function inferCode(text) {
46
57
  || normalized.includes('already activated on maximum')) {
47
58
  return LicenseErrorCode.ACTIVATION_LIMIT_REACHED;
48
59
  }
60
+ if (normalized.includes('rate limited')
61
+ || normalized.includes('rate_limited')
62
+ || normalized.includes('too many requests')
63
+ || normalized.includes('too_many_requests')
64
+ || normalized.includes('429')) {
65
+ return LicenseErrorCode.RATE_LIMITED;
66
+ }
67
+ if (normalized.includes('expired') || normalized.includes('expiration') || normalized.includes('license_expired')) {
68
+ return LicenseErrorCode.LICENSE_EXPIRED;
69
+ }
70
+ if (normalized.includes('revoked')
71
+ || normalized.includes('license_revoked')
72
+ || normalized.includes('cancelled')
73
+ || normalized.includes('canceled')) {
74
+ return LicenseErrorCode.LICENSE_REVOKED;
75
+ }
49
76
  if (normalized.includes('invalid license')
50
77
  || normalized.includes('not found')
51
78
  || normalized.includes('not valid')
52
79
  || normalized.includes('license key is invalid')) {
53
80
  return LicenseErrorCode.INVALID_LICENSE;
54
81
  }
55
- if (normalized.includes('product mismatch') || normalized.includes('wrong product')) {
82
+ if (normalized.includes('product mismatch')
83
+ || normalized.includes('product_mismatch')
84
+ || normalized.includes('product_not_allowed')
85
+ || normalized.includes('wrong product')) {
56
86
  return LicenseErrorCode.PRODUCT_MISMATCH;
57
87
  }
58
88
  if (normalized.includes('network')
@@ -73,7 +103,9 @@ function mapLicenseError(error, fallbackMessage = '라이선스 처리 중 알
73
103
  const text = asText(error);
74
104
  const message = text.trim() || fallbackMessage;
75
105
  const code = inferCode(message);
76
- return new LicenseCoreError(code, message, error, code === LicenseErrorCode.NETWORK_ERROR || code === LicenseErrorCode.SERVER_ERROR);
106
+ return new LicenseCoreError(code, message, error, code === LicenseErrorCode.NETWORK_ERROR
107
+ || code === LicenseErrorCode.SERVER_ERROR
108
+ || code === LicenseErrorCode.RATE_LIMITED);
77
109
  }
78
110
  function isActivationLimitReached(activationLimit, activationUsage) {
79
111
  if (activationLimit <= 0) {
@@ -85,10 +117,19 @@ function isActivationLimitReached(activationLimit, activationUsage) {
85
117
  const STORAGE_KEYS = {
86
118
  paymentStatus: 'paymentStatus',
87
119
  licenseKey: 'licenseKey',
120
+ instanceId: 'instanceId',
88
121
  activationLimit: 'activationLimit',
89
122
  activationUsage: 'activationUsage',
90
123
  usageCount: 'usageCount',
124
+ previousUsageCount: 'previousUsageCount',
125
+ usageWindowKey: 'usageWindowKey',
126
+ planTier: 'planTier',
91
127
  lastVerifiedAt: 'lastVerifiedAt',
128
+ licenseStatus: 'licenseStatus',
129
+ licenseSource: 'licenseSource',
130
+ accessScope: 'accessScope',
131
+ expiresAt: 'expiresAt',
132
+ validationMessage: 'validationMessage',
92
133
  };
93
134
  const DEFAULT_USAGE_LIMIT = 20;
94
135
  const MASTER_CODE_ACTIVATION_LIMIT = 999999;
@@ -107,6 +148,31 @@ function toNonNegativeNumber(value, fallback = 0) {
107
148
  function toText(value) {
108
149
  return typeof value === 'string' ? value : '';
109
150
  }
151
+ function validationMessage(result) {
152
+ if (result.message) {
153
+ return result.message;
154
+ }
155
+ const status = (result.licenseStatus || result.errorCode || '').toLowerCase();
156
+ switch (status) {
157
+ case 'revoked':
158
+ case 'license_revoked':
159
+ return '이 멤버십 라이선스는 철회되었습니다.';
160
+ case 'expired':
161
+ case 'license_expired':
162
+ return '멤버십 라이선스가 만료되었습니다. YouTube 커뮤니티에서 이번 달 키를 확인해 주세요.';
163
+ case 'not_started':
164
+ return '이 멤버십 라이선스는 아직 시작되지 않았습니다.';
165
+ case 'rate_limited':
166
+ case 'rate_limited_error':
167
+ return '라이선스 확인 요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.';
168
+ case 'product_not_allowed':
169
+ return '이 라이선스는 현재 플러그인에서 사용할 수 없습니다.';
170
+ case 'network_error':
171
+ return '라이선스 서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.';
172
+ default:
173
+ return '라이선스가 더 이상 유효하지 않습니다.';
174
+ }
175
+ }
110
176
  function toPaymentStatus(value) {
111
177
  if (value === 'PAID' || value === 'UNPAID' || value === 'FREE') {
112
178
  return value;
@@ -116,6 +182,59 @@ function toPaymentStatus(value) {
116
182
  function normalizeProductId(productId) {
117
183
  return productId.trim().toLowerCase();
118
184
  }
185
+ function normalizeTier(tier) {
186
+ return tier.trim() || 'FREE';
187
+ }
188
+ function normalizeUsageReset(reset) {
189
+ if (reset === 'daily' || reset === 'monthly' || reset === 'none') {
190
+ return reset;
191
+ }
192
+ return 'none';
193
+ }
194
+ function toDateKey(date, period) {
195
+ if (period === 'none') {
196
+ return 'lifetime';
197
+ }
198
+ const year = date.getUTCFullYear();
199
+ const month = `${date.getUTCMonth() + 1}`.padStart(2, '0');
200
+ const day = `${date.getUTCDate()}`.padStart(2, '0');
201
+ if (period === 'monthly') {
202
+ return `${year}-${month}`;
203
+ }
204
+ return `${year}-${month}-${day}`;
205
+ }
206
+ function normalizePolicy(config) {
207
+ const rawPolicy = config.policy || {};
208
+ const freeTier = normalizeTier(rawPolicy.freeTier || 'FREE');
209
+ const paidTier = normalizeTier(rawPolicy.paidTier || 'PAID');
210
+ const defaultTier = normalizeTier(rawPolicy.defaultTier || freeTier);
211
+ const usageByTier = {
212
+ [freeTier]: {
213
+ limit: Math.max(config.usageLimit ?? DEFAULT_USAGE_LIMIT, 0),
214
+ reset: 'none',
215
+ },
216
+ [paidTier]: {
217
+ limit: 'unlimited',
218
+ reset: 'none',
219
+ },
220
+ ...(rawPolicy.usageByTier || {}),
221
+ };
222
+ Object.keys(usageByTier).forEach((tier) => {
223
+ usageByTier[tier] = {
224
+ limit: usageByTier[tier].limit === 'unlimited'
225
+ ? 'unlimited'
226
+ : Math.max(toNonNegativeNumber(usageByTier[tier].limit, 0), 0),
227
+ reset: normalizeUsageReset(usageByTier[tier].reset),
228
+ };
229
+ });
230
+ return {
231
+ defaultTier,
232
+ freeTier,
233
+ paidTier,
234
+ usageByTier,
235
+ featuresByTier: rawPolicy.featuresByTier || {},
236
+ };
237
+ }
119
238
  class LicenseCore {
120
239
  constructor(config, deps) {
121
240
  this.config = config;
@@ -129,14 +248,117 @@ class LicenseCore {
129
248
  if (!config.instanceName.trim()) {
130
249
  throw new Error('LicenseConfig.instanceName is required.');
131
250
  }
132
- this.usageLimit = Math.max(config.usageLimit ?? DEFAULT_USAGE_LIMIT, 0);
251
+ this.policy = normalizePolicy(config);
133
252
  this.storagePrefix = config.storagePrefix?.trim() || `${config.instanceName}:${config.productId}`;
134
253
  }
135
254
  async initialize() {
136
- return this.loadState();
255
+ let state = await this.getStatus();
256
+ if (state.paymentStatus === 'PAID' && state.licenseKey && state.licenseKey !== this.config.masterCode) {
257
+ try {
258
+ const validateResult = this.deps.transport.validate
259
+ ? await this.deps.transport.validate({
260
+ licenseKey: state.licenseKey,
261
+ productId: this.config.productId,
262
+ instanceName: this.config.instanceName,
263
+ instanceId: state.instanceId,
264
+ })
265
+ : await this.deps.transport.verify({
266
+ licenseKey: state.licenseKey,
267
+ productId: this.config.productId,
268
+ instanceName: this.config.instanceName,
269
+ });
270
+ if (!validateResult.valid) {
271
+ state = await this.markValidationFailure(state, validationMessage(validateResult), validateResult.errorCode || validateResult.licenseStatus);
272
+ }
273
+ else {
274
+ state = await this.applyValidationResult(state, validateResult);
275
+ }
276
+ }
277
+ catch (error) {
278
+ const mappedError = mapLicenseError(error, '라이선스 검증 요청에 실패했습니다.');
279
+ state = await this.markValidationFailure(state, mappedError.message, mappedError.code);
280
+ }
281
+ }
282
+ return this.applyUsageWindowResetIfNeeded(state);
283
+ }
284
+ async resetLicenseState() {
285
+ const current = await this.getStatus();
286
+ const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
287
+ const previousUsageCount = current.previousUsageCount ?? 0;
288
+ const nextState = {
289
+ ...current,
290
+ paymentStatus: 'FREE',
291
+ planTier: this.policy.freeTier,
292
+ usageWindowKey: toDateKey(new Date(), freeReset),
293
+ licenseKey: '',
294
+ instanceId: '',
295
+ activationLimit: 0,
296
+ activationUsage: 0,
297
+ usageCount: previousUsageCount,
298
+ lastVerifiedAt: undefined,
299
+ licenseStatus: undefined,
300
+ licenseSource: undefined,
301
+ accessScope: undefined,
302
+ expiresAt: undefined,
303
+ validationMessage: undefined,
304
+ };
305
+ await this.persistState(nextState);
306
+ return nextState;
307
+ }
308
+ async applyValidationResult(current, result) {
309
+ const nextState = {
310
+ ...current,
311
+ paymentStatus: 'PAID',
312
+ activationLimit: result.activationLimit ?? current.activationLimit,
313
+ activationUsage: result.activationUsage ?? current.activationUsage,
314
+ licenseStatus: result.licenseStatus || 'active',
315
+ licenseSource: result.licenseSource || current.licenseSource,
316
+ accessScope: result.accessScope || current.accessScope,
317
+ expiresAt: result.expiresAt ?? current.expiresAt,
318
+ validationMessage: result.message || undefined,
319
+ lastVerifiedAt: new Date().toISOString(),
320
+ };
321
+ await this.persistState(nextState);
322
+ return nextState;
323
+ }
324
+ async markValidationFailure(current, message, errorCode) {
325
+ const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
326
+ const nextState = {
327
+ ...current,
328
+ paymentStatus: 'UNPAID',
329
+ planTier: this.policy.freeTier,
330
+ usageWindowKey: toDateKey(new Date(), freeReset),
331
+ activationLimit: 0,
332
+ activationUsage: 0,
333
+ usageCount: current.previousUsageCount ?? current.usageCount,
334
+ licenseStatus: errorCode || 'invalid',
335
+ accessScope: undefined,
336
+ expiresAt: undefined,
337
+ validationMessage: message,
338
+ lastVerifiedAt: new Date().toISOString(),
339
+ };
340
+ await this.persistState(nextState);
341
+ return nextState;
137
342
  }
138
343
  async getStatus() {
139
- return this.loadState();
344
+ const state = await this.loadState();
345
+ return this.applyUsageWindowResetIfNeeded(state);
346
+ }
347
+ async getLicenseSummary() {
348
+ const state = await this.getStatus();
349
+ return {
350
+ paymentStatus: state.paymentStatus,
351
+ planTier: this.resolveTier(state),
352
+ licenseKey: state.licenseKey,
353
+ hasLicenseKey: Boolean(state.licenseKey),
354
+ isLicenseRegistered: this.isLicenseRegisteredState(state),
355
+ licenseStatus: state.licenseStatus,
356
+ licenseSource: state.licenseSource,
357
+ accessScope: state.accessScope,
358
+ expiresAt: state.expiresAt,
359
+ lastVerifiedAt: state.lastVerifiedAt,
360
+ validationMessage: state.validationMessage,
361
+ };
140
362
  }
141
363
  async verifyLicense(licenseKey) {
142
364
  const trimmedLicenseKey = licenseKey.trim();
@@ -144,18 +366,27 @@ class LicenseCore {
144
366
  throw new LicenseCoreError(LicenseErrorCode.INVALID_LICENSE, '라이선스 키를 입력해 주세요.');
145
367
  }
146
368
  if (this.config.masterCode && trimmedLicenseKey === this.config.masterCode) {
147
- const current = await this.loadState();
369
+ const current = await this.getStatus();
148
370
  const nextState = {
149
371
  ...current,
150
372
  paymentStatus: 'PAID',
373
+ planTier: this.policy.paidTier,
374
+ usageWindowKey: toDateKey(new Date(), this.getUsagePolicy(this.policy.paidTier).reset),
151
375
  licenseKey: trimmedLicenseKey,
376
+ instanceId: 'master-instance',
152
377
  activationLimit: MASTER_CODE_ACTIVATION_LIMIT,
153
378
  activationUsage: 0,
379
+ previousUsageCount: current.usageCount,
154
380
  lastVerifiedAt: new Date().toISOString(),
381
+ licenseStatus: 'active',
382
+ licenseSource: 'master',
383
+ accessScope: 'all-products',
384
+ expiresAt: undefined,
385
+ validationMessage: undefined,
155
386
  };
156
387
  await this.persistState(nextState);
157
388
  await this.notify('마스터 코드로 라이선스가 활성화되었습니다.');
158
- return nextState;
389
+ return this.applyUsageWindowResetIfNeeded(nextState);
159
390
  }
160
391
  let verifyResult;
161
392
  try {
@@ -169,7 +400,7 @@ class LicenseCore {
169
400
  throw mapLicenseError(error, '라이선스 검증 요청에 실패했습니다.');
170
401
  }
171
402
  if (!verifyResult.valid) {
172
- throw mapLicenseError(verifyResult.raw ?? verifyResult.message, '유효하지 않은 라이선스 키입니다.');
403
+ throw mapLicenseError(verifyResult.raw ?? { message: verifyResult.message, errorCode: verifyResult.errorCode }, '유효하지 않은 라이선스 키입니다.');
173
404
  }
174
405
  if (verifyResult.productId
175
406
  && normalizeProductId(verifyResult.productId) !== normalizeProductId(this.config.productId)) {
@@ -180,21 +411,32 @@ class LicenseCore {
180
411
  if (isActivationLimitReached(activationLimit, activationUsage)) {
181
412
  throw new LicenseCoreError(LicenseErrorCode.ACTIVATION_LIMIT_REACHED, '활성화 가능한 기기 수를 모두 사용했습니다. 기존 기기를 비활성화한 뒤 다시 시도해 주세요.', verifyResult);
182
413
  }
183
- const current = await this.loadState();
414
+ const current = await this.getStatus();
415
+ const planTier = normalizeTier(verifyResult.planTier || this.policy.paidTier);
416
+ const usageReset = this.getUsagePolicy(planTier).reset;
184
417
  const nextState = {
185
418
  ...current,
186
419
  paymentStatus: 'PAID',
187
- licenseKey: trimmedLicenseKey,
420
+ planTier,
421
+ usageWindowKey: toDateKey(new Date(), usageReset),
422
+ licenseKey: verifyResult.licenseKey || trimmedLicenseKey,
423
+ instanceId: verifyResult.instanceId || '',
188
424
  activationLimit,
189
425
  activationUsage,
426
+ previousUsageCount: current.usageCount,
190
427
  lastVerifiedAt: new Date().toISOString(),
428
+ licenseStatus: verifyResult.licenseStatus || 'active',
429
+ licenseSource: verifyResult.licenseSource,
430
+ accessScope: verifyResult.accessScope,
431
+ expiresAt: verifyResult.expiresAt,
432
+ validationMessage: verifyResult.message || undefined,
191
433
  };
192
434
  await this.persistState(nextState);
193
435
  await this.notify(verifyResult.message || '라이선스가 활성화되었습니다.');
194
- return nextState;
436
+ return this.applyUsageWindowResetIfNeeded(nextState);
195
437
  }
196
438
  async deactivateLicense() {
197
- const current = await this.loadState();
439
+ const current = await this.getStatus();
198
440
  if (!current.licenseKey) {
199
441
  return current;
200
442
  }
@@ -202,6 +444,7 @@ class LicenseCore {
202
444
  licenseKey: current.licenseKey,
203
445
  productId: this.config.productId,
204
446
  instanceName: this.config.instanceName,
447
+ instanceId: current.instanceId,
205
448
  };
206
449
  if (!this.config.masterCode || current.licenseKey !== this.config.masterCode) {
207
450
  try {
@@ -214,24 +457,19 @@ class LicenseCore {
214
457
  throw mapLicenseError(error, '라이선스 비활성화 요청에 실패했습니다.');
215
458
  }
216
459
  }
217
- const nextState = {
218
- ...current,
219
- paymentStatus: 'FREE',
220
- licenseKey: '',
221
- activationLimit: 0,
222
- activationUsage: 0,
223
- lastVerifiedAt: undefined,
224
- };
225
- await this.persistState(nextState);
460
+ const nextState = await this.resetLicenseState();
226
461
  await this.notify('라이선스가 비활성화되었습니다.');
227
- return nextState;
462
+ return this.applyUsageWindowResetIfNeeded(nextState);
228
463
  }
229
464
  async incrementUsage(amount = 1) {
230
- const current = await this.loadState();
465
+ const current = await this.getStatus();
231
466
  if (current.paymentStatus === 'PAID') {
232
467
  return current;
233
468
  }
234
469
  const increasedAmount = Math.max(0, amount);
470
+ if (increasedAmount === 0) {
471
+ return current;
472
+ }
235
473
  const nextState = {
236
474
  ...current,
237
475
  usageCount: current.usageCount + increasedAmount,
@@ -239,64 +477,186 @@ class LicenseCore {
239
477
  await this.persistState(nextState);
240
478
  return nextState;
241
479
  }
242
- async canUseFeature() {
243
- const state = await this.loadState();
244
- return this.resolveUsageAccess(state);
480
+ async canUseFeature(featureKey) {
481
+ const state = await this.getStatus();
482
+ return this.resolveUsageAccess(state, featureKey);
245
483
  }
246
- resolveUsageAccess(state) {
247
- if (state.paymentStatus === 'PAID') {
484
+ async consumeUsage(amount = 1, featureKey) {
485
+ const state = await this.getStatus();
486
+ const access = this.resolveUsageAccess(state, featureKey);
487
+ if (!access.allowed || amount <= 0) {
488
+ return { state, access };
489
+ }
490
+ const updatedState = await this.incrementUsage(amount);
491
+ const updatedAccess = this.resolveUsageAccess(updatedState, featureKey);
492
+ return {
493
+ state: updatedState,
494
+ access: updatedAccess,
495
+ };
496
+ }
497
+ resolveUsageAccess(state, featureKey) {
498
+ const tier = this.resolveTier(state);
499
+ const usagePolicy = this.getUsagePolicy(tier);
500
+ const featureAllowed = this.isFeatureAllowed(tier, featureKey);
501
+ const isLicensed = this.isLicenseRegisteredState(state);
502
+ const used = state.usageCount;
503
+ if (usagePolicy.limit === 'unlimited') {
248
504
  return {
249
- allowed: true,
505
+ allowed: featureAllowed,
250
506
  remaining: Number.POSITIVE_INFINITY,
507
+ used,
508
+ limit: 'unlimited',
509
+ reset: usagePolicy.reset,
510
+ tier,
511
+ hasLicenseKey: Boolean(state.licenseKey),
512
+ isLicenseRegistered: isLicensed,
513
+ featureAllowed,
514
+ featureKey,
515
+ reason: featureAllowed ? undefined : 'FEATURE_NOT_ALLOWED',
251
516
  };
252
517
  }
253
- const remaining = Math.max(this.usageLimit - state.usageCount, 0);
518
+ const remaining = Math.max(usagePolicy.limit - used, 0);
519
+ const usageAllowed = remaining > 0;
520
+ const allowed = featureAllowed && usageAllowed;
254
521
  return {
255
- allowed: remaining > 0,
522
+ allowed,
256
523
  remaining,
524
+ used,
525
+ limit: usagePolicy.limit,
526
+ reset: usagePolicy.reset,
527
+ tier,
528
+ hasLicenseKey: Boolean(state.licenseKey),
529
+ isLicenseRegistered: isLicensed,
530
+ featureAllowed,
531
+ featureKey,
532
+ reason: featureAllowed ? (usageAllowed ? undefined : 'USAGE_LIMIT_REACHED') : 'FEATURE_NOT_ALLOWED',
257
533
  };
258
534
  }
259
535
  getUsageLimit() {
260
- return this.usageLimit;
536
+ const freePolicy = this.getUsagePolicy(this.policy.freeTier);
537
+ return freePolicy.limit === 'unlimited' ? Number.POSITIVE_INFINITY : freePolicy.limit;
261
538
  }
262
539
  getStoragePrefix() {
263
540
  return this.storagePrefix;
264
541
  }
542
+ resolveTier(state) {
543
+ const storedTier = normalizeTier(state.planTier || '');
544
+ if (storedTier) {
545
+ return storedTier;
546
+ }
547
+ return state.paymentStatus === 'PAID' ? this.policy.paidTier : this.policy.freeTier;
548
+ }
549
+ getUsagePolicy(tier) {
550
+ return this.policy.usageByTier[tier]
551
+ || this.policy.usageByTier[this.policy.defaultTier]
552
+ || { limit: 'unlimited', reset: 'none' };
553
+ }
554
+ isFeatureAllowed(tier, featureKey) {
555
+ if (!featureKey) {
556
+ return true;
557
+ }
558
+ const policy = this.policy.featuresByTier[tier];
559
+ if (!policy) {
560
+ return true;
561
+ }
562
+ if (policy.deny?.includes(featureKey)) {
563
+ return false;
564
+ }
565
+ if (policy.allow && policy.allow.length > 0) {
566
+ return policy.allow.includes(featureKey);
567
+ }
568
+ return policy.defaultAllowed ?? true;
569
+ }
570
+ isLicenseRegisteredState(state) {
571
+ const status = state.licenseStatus?.toLowerCase();
572
+ const invalidStatuses = ['invalid', 'inactive', 'expired', 'revoked', 'unpaid'];
573
+ return state.paymentStatus === 'PAID'
574
+ && Boolean(state.licenseKey)
575
+ && !invalidStatuses.includes(status || '');
576
+ }
265
577
  storageKey(name) {
266
578
  return `${this.storagePrefix}:${STORAGE_KEYS[name]}`;
267
579
  }
268
580
  async loadState() {
269
581
  try {
270
- const [paymentStatus, licenseKey, activationLimit, activationUsage, usageCount, lastVerifiedAt,] = await Promise.all([
582
+ const [paymentStatus, licenseKey, instanceId, activationLimit, activationUsage, usageCount, previousUsageCount, usageWindowKey, planTier, lastVerifiedAt, licenseStatus, licenseSource, accessScope, expiresAt, validationMessage,] = await Promise.all([
271
583
  this.deps.storage.get(this.storageKey('paymentStatus')),
272
584
  this.deps.storage.get(this.storageKey('licenseKey')),
585
+ this.deps.storage.get(this.storageKey('instanceId')),
273
586
  this.deps.storage.get(this.storageKey('activationLimit')),
274
587
  this.deps.storage.get(this.storageKey('activationUsage')),
275
588
  this.deps.storage.get(this.storageKey('usageCount')),
589
+ this.deps.storage.get(this.storageKey('previousUsageCount')),
590
+ this.deps.storage.get(this.storageKey('usageWindowKey')),
591
+ this.deps.storage.get(this.storageKey('planTier')),
276
592
  this.deps.storage.get(this.storageKey('lastVerifiedAt')),
593
+ this.deps.storage.get(this.storageKey('licenseStatus')),
594
+ this.deps.storage.get(this.storageKey('licenseSource')),
595
+ this.deps.storage.get(this.storageKey('accessScope')),
596
+ this.deps.storage.get(this.storageKey('expiresAt')),
597
+ this.deps.storage.get(this.storageKey('validationMessage')),
277
598
  ]);
599
+ const normalizedPaymentStatus = toPaymentStatus(paymentStatus);
600
+ const resolvedTier = normalizeTier(toText(planTier)
601
+ || (normalizedPaymentStatus === 'PAID' ? this.policy.paidTier : this.policy.freeTier));
602
+ const usageReset = this.getUsagePolicy(resolvedTier).reset;
278
603
  return {
279
- paymentStatus: toPaymentStatus(paymentStatus),
604
+ paymentStatus: normalizedPaymentStatus,
280
605
  licenseKey: toText(licenseKey),
606
+ instanceId: toText(instanceId),
281
607
  activationLimit: toNonNegativeNumber(activationLimit, 0),
282
608
  activationUsage: toNonNegativeNumber(activationUsage, 0),
283
609
  usageCount: toNonNegativeNumber(usageCount, 0),
610
+ previousUsageCount: toNonNegativeNumber(previousUsageCount, 0),
611
+ usageWindowKey: toText(usageWindowKey) || toDateKey(new Date(), usageReset),
612
+ planTier: resolvedTier,
284
613
  lastVerifiedAt: toText(lastVerifiedAt) || undefined,
614
+ licenseStatus: toText(licenseStatus) || undefined,
615
+ licenseSource: toText(licenseSource) || undefined,
616
+ accessScope: toText(accessScope) || undefined,
617
+ expiresAt: toText(expiresAt) || undefined,
618
+ validationMessage: toText(validationMessage) || undefined,
285
619
  };
286
620
  }
287
621
  catch (error) {
288
622
  throw new LicenseCoreError(LicenseErrorCode.STORAGE_ERROR, '라이선스 상태를 불러오지 못했습니다.', error, true);
289
623
  }
290
624
  }
625
+ async applyUsageWindowResetIfNeeded(state) {
626
+ const tier = this.resolveTier(state);
627
+ const usageReset = this.getUsagePolicy(tier).reset;
628
+ const expectedWindowKey = toDateKey(new Date(), usageReset);
629
+ const requiresUsageReset = usageReset !== 'none' && state.usageWindowKey !== expectedWindowKey;
630
+ const requiresWindowMigration = state.usageWindowKey !== expectedWindowKey;
631
+ if (!requiresUsageReset && !requiresWindowMigration) {
632
+ return state;
633
+ }
634
+ const nextState = {
635
+ ...state,
636
+ usageWindowKey: expectedWindowKey,
637
+ usageCount: requiresUsageReset ? 0 : state.usageCount,
638
+ };
639
+ await this.persistState(nextState);
640
+ return nextState;
641
+ }
291
642
  async persistState(state) {
292
643
  try {
293
644
  await Promise.all([
294
645
  this.deps.storage.set(this.storageKey('paymentStatus'), state.paymentStatus),
295
646
  this.deps.storage.set(this.storageKey('licenseKey'), state.licenseKey),
647
+ this.deps.storage.set(this.storageKey('instanceId'), state.instanceId || ''),
296
648
  this.deps.storage.set(this.storageKey('activationLimit'), state.activationLimit),
297
649
  this.deps.storage.set(this.storageKey('activationUsage'), state.activationUsage),
298
650
  this.deps.storage.set(this.storageKey('usageCount'), state.usageCount),
651
+ this.deps.storage.set(this.storageKey('previousUsageCount'), state.previousUsageCount || 0),
652
+ this.deps.storage.set(this.storageKey('usageWindowKey'), state.usageWindowKey),
653
+ this.deps.storage.set(this.storageKey('planTier'), state.planTier),
299
654
  this.deps.storage.set(this.storageKey('lastVerifiedAt'), state.lastVerifiedAt || ''),
655
+ this.deps.storage.set(this.storageKey('licenseStatus'), state.licenseStatus || ''),
656
+ this.deps.storage.set(this.storageKey('licenseSource'), state.licenseSource || ''),
657
+ this.deps.storage.set(this.storageKey('accessScope'), state.accessScope || ''),
658
+ this.deps.storage.set(this.storageKey('expiresAt'), state.expiresAt || ''),
659
+ this.deps.storage.set(this.storageKey('validationMessage'), state.validationMessage || ''),
300
660
  ]);
301
661
  }
302
662
  catch (error) {
@@ -346,18 +706,47 @@ function readNumberField(raw, keys) {
346
706
  }
347
707
  return undefined;
348
708
  }
349
- function asRecord(value) {
709
+ function readBooleanField(raw, keys) {
710
+ if (!raw || typeof raw !== 'object') {
711
+ return undefined;
712
+ }
713
+ for (const key of keys) {
714
+ const value = raw[key];
715
+ if (typeof value === 'boolean') {
716
+ return value;
717
+ }
718
+ if (typeof value === 'string') {
719
+ const normalized = value.trim().toLowerCase();
720
+ if (['true', '1', 'yes', 'active', 'valid', 'success'].includes(normalized)) {
721
+ return true;
722
+ }
723
+ if (['false', '0', 'no', 'inactive', 'invalid', 'revoked', 'expired', 'error'].includes(normalized)) {
724
+ return false;
725
+ }
726
+ }
727
+ }
728
+ return undefined;
729
+ }
730
+ function asRecord$1(value) {
350
731
  return value && typeof value === 'object' ? value : {};
351
732
  }
352
733
  function defaultNormalizeVerifyResponse(raw) {
353
- const body = asRecord(raw);
354
- const valid = Boolean(body.valid
355
- ?? body.success
356
- ?? body.active
357
- ?? (typeof body.status === 'string' && body.status.toLowerCase() === 'active'));
734
+ const body = asRecord$1(raw);
735
+ const licenseStatus = readTextField(body, ['licenseStatus', 'license_status', 'status'])
736
+ || readTextField(asRecord$1(body.meta), ['license_status', 'status']);
737
+ const statusValid = typeof licenseStatus === 'string'
738
+ ? licenseStatus.toLowerCase() === 'active'
739
+ : undefined;
740
+ const valid = readBooleanField(body, ['valid', 'success', 'active'])
741
+ ?? statusValid
742
+ ?? false;
358
743
  const message = readTextField(body, ['message', 'error', 'detail']);
359
- const nestedLicenseKey = asRecord(body.license_key);
360
- const nestedMeta = asRecord(body.meta);
744
+ const nestedLicenseKey = asRecord$1(body.license_key);
745
+ const nestedMeta = asRecord$1(body.meta);
746
+ const nestedInstance = asRecord$1(body.instance);
747
+ const instanceId = readTextField(body, ['instanceId', 'instance_id'])
748
+ || readTextField(nestedInstance, ['id'])
749
+ || readTextField(nestedLicenseKey, ['instance_id']);
361
750
  const productId = readTextField(body, ['productId', 'product_id'])
362
751
  || readTextField(nestedMeta, ['product_id'])
363
752
  || readTextField(nestedLicenseKey, ['product_id']);
@@ -367,17 +756,37 @@ function defaultNormalizeVerifyResponse(raw) {
367
756
  const activationUsage = readNumberField(body, ['activationUsage', 'activation_usage'])
368
757
  ?? readNumberField(nestedMeta, ['activation_usage'])
369
758
  ?? readNumberField(nestedLicenseKey, ['activation_usage']);
759
+ const planTier = readTextField(body, ['planTier', 'tier', 'plan'])
760
+ || readTextField(nestedMeta, ['tier', 'plan']);
761
+ const licenseSource = readTextField(body, ['licenseSource', 'license_source', 'source'])
762
+ || readTextField(nestedMeta, ['license_source', 'source'])
763
+ || readTextField(nestedLicenseKey, ['license_source', 'source']);
764
+ const accessScope = readTextField(body, ['accessScope', 'access_scope', 'scope'])
765
+ || readTextField(nestedMeta, ['access_scope', 'scope'])
766
+ || readTextField(nestedLicenseKey, ['access_scope', 'scope']);
767
+ const expiresAt = readTextField(body, ['expiresAt', 'expires_at', 'expiration', 'expiration_date'])
768
+ || readTextField(nestedMeta, ['expires_at', 'expiration', 'expiration_date'])
769
+ || readTextField(nestedLicenseKey, ['expires_at', 'expiration', 'expiration_date']);
770
+ const errorCode = readTextField(body, ['errorCode', 'error_code', 'code'])
771
+ || readTextField(nestedMeta, ['error_code', 'code']);
370
772
  return {
371
773
  valid,
372
774
  message,
373
775
  productId,
374
776
  activationLimit,
375
777
  activationUsage,
778
+ instanceId,
779
+ planTier,
780
+ licenseStatus,
781
+ licenseSource,
782
+ accessScope,
783
+ expiresAt,
784
+ errorCode,
376
785
  raw,
377
786
  };
378
787
  }
379
788
  function defaultNormalizeDeactivateResponse(raw) {
380
- const body = asRecord(raw);
789
+ const body = asRecord$1(raw);
381
790
  const success = Boolean(body.success
382
791
  ?? body.deactivated
383
792
  ?? body.valid
@@ -388,6 +797,14 @@ function defaultNormalizeDeactivateResponse(raw) {
388
797
  raw,
389
798
  };
390
799
  }
800
+ class LicenseTransportHttpError extends Error {
801
+ constructor(status, body, message) {
802
+ super(message);
803
+ this.status = status;
804
+ this.body = body;
805
+ this.name = 'LicenseTransportHttpError';
806
+ }
807
+ }
391
808
  class FetchLicenseTransport {
392
809
  constructor(config) {
393
810
  this.config = config;
@@ -401,6 +818,15 @@ class FetchLicenseTransport {
401
818
  const normalize = this.config.normalizeVerifyResponse ?? defaultNormalizeVerifyResponse;
402
819
  return normalize(raw);
403
820
  }
821
+ async validate(payload) {
822
+ const url = this.config.validateUrl || this.config.verifyUrl;
823
+ const body = this.config.buildValidateBody
824
+ ? this.config.buildValidateBody(payload)
825
+ : payload;
826
+ const raw = await this.postJson(url, body);
827
+ const normalize = this.config.normalizeVerifyResponse ?? defaultNormalizeVerifyResponse;
828
+ return normalize(raw);
829
+ }
404
830
  async deactivate(payload) {
405
831
  const url = this.config.deactivateUrl || this.config.verifyUrl;
406
832
  const body = this.config.buildDeactivateBody
@@ -430,8 +856,143 @@ class FetchLicenseTransport {
430
856
  signal: abortController?.signal,
431
857
  });
432
858
  if (!response.ok) {
433
- const errorText = await response.text();
434
- throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`);
859
+ let errorBody;
860
+ try {
861
+ errorBody = await response.json();
862
+ }
863
+ catch {
864
+ errorBody = await response.text();
865
+ }
866
+ const errorRecord = asRecord$1(errorBody);
867
+ const errorMessage = readTextField(errorRecord, ['message', 'error', 'detail'])
868
+ || (typeof errorBody === 'string' ? errorBody : '')
869
+ || response.statusText;
870
+ throw new LicenseTransportHttpError(response.status, errorBody, `HTTP ${response.status}: ${errorMessage}`);
871
+ }
872
+ return await response.json();
873
+ }
874
+ finally {
875
+ if (timeoutId) {
876
+ clearTimeout(timeoutId);
877
+ }
878
+ }
879
+ }
880
+ }
881
+
882
+ function asRecord(value) {
883
+ return value && typeof value === 'object' ? value : {};
884
+ }
885
+ function readText(raw, keys) {
886
+ const body = asRecord(raw);
887
+ for (const key of keys) {
888
+ const value = body[key];
889
+ if (typeof value === 'string' && value.trim() !== '') {
890
+ return value;
891
+ }
892
+ }
893
+ return '';
894
+ }
895
+ function readNumber(raw, keys) {
896
+ const body = asRecord(raw);
897
+ for (const key of keys) {
898
+ const value = body[key];
899
+ if (typeof value === 'number' && Number.isFinite(value)) {
900
+ return value;
901
+ }
902
+ if (typeof value === 'string' && value.trim() !== '') {
903
+ const numeric = Number(value);
904
+ if (Number.isFinite(numeric)) {
905
+ return numeric;
906
+ }
907
+ }
908
+ }
909
+ return undefined;
910
+ }
911
+ function joinUrl(baseUrl, path) {
912
+ return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
913
+ }
914
+ class DesignbaseAccountAuthClient {
915
+ constructor(config) {
916
+ this.config = config;
917
+ if (!config.baseUrl.trim()) {
918
+ throw new Error('DesignbaseAccountAuthConfig.baseUrl is required.');
919
+ }
920
+ this.fetchFn = config.fetchFn ?? fetch;
921
+ }
922
+ async start(payload) {
923
+ const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.startPath || '/wp-json/custom/v1/figma/auth/start'), {
924
+ plugin_code: payload.pluginCode,
925
+ device_id: payload.deviceId,
926
+ return_url: payload.returnUrl || '',
927
+ });
928
+ return {
929
+ sessionId: readText(raw, ['session_id', 'sessionId']),
930
+ pollToken: readText(raw, ['poll_token', 'pollToken']),
931
+ authUrl: readText(raw, ['auth_url', 'authUrl']),
932
+ expiresIn: readNumber(raw, ['expires_in', 'expiresIn']) ?? 600,
933
+ pollInterval: readNumber(raw, ['poll_interval', 'pollInterval']) ?? 3,
934
+ raw,
935
+ };
936
+ }
937
+ async poll(sessionId, pollToken) {
938
+ const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.pollPath || '/wp-json/custom/v1/figma/auth/poll'), {
939
+ session_id: sessionId,
940
+ poll_token: pollToken,
941
+ });
942
+ const status = readText(raw, ['status']) || 'pending';
943
+ const allowed = Boolean(asRecord(raw).allowed || status === 'active');
944
+ const tokens = asRecord(asRecord(raw).tokens);
945
+ const actions = asRecord(asRecord(raw).actions);
946
+ return {
947
+ status,
948
+ allowed,
949
+ pluginCode: readText(raw, ['plugin_code', 'pluginCode']),
950
+ productId: readText(raw, ['product_id', 'productId']) || undefined,
951
+ planTier: readText(raw, ['plan_tier', 'planTier']) || undefined,
952
+ licenseKey: readText(raw, ['license_key', 'licenseKey']) || undefined,
953
+ accessToken: readText(tokens, ['access_token', 'accessToken']) || undefined,
954
+ refreshToken: readText(tokens, ['refresh_token', 'refreshToken']) || undefined,
955
+ expiresIn: readNumber(tokens, ['expires_in', 'expiresIn']),
956
+ message: readText(raw, ['message']) || undefined,
957
+ actions,
958
+ raw,
959
+ };
960
+ }
961
+ async saveTokens(result) {
962
+ if (!this.config.storage || !result.accessToken) {
963
+ return;
964
+ }
965
+ const prefix = this.config.storagePrefix || 'designbase-account-auth';
966
+ await Promise.all([
967
+ this.config.storage.set(`${prefix}:accessToken`, result.accessToken),
968
+ this.config.storage.set(`${prefix}:refreshToken`, result.refreshToken || ''),
969
+ this.config.storage.set(`${prefix}:expiresAt`, Date.now() + ((result.expiresIn || 1800) * 1000)),
970
+ ]);
971
+ }
972
+ async getAccessToken() {
973
+ if (!this.config.storage) {
974
+ return '';
975
+ }
976
+ const prefix = this.config.storagePrefix || 'designbase-account-auth';
977
+ const token = await this.config.storage.get(`${prefix}:accessToken`);
978
+ return typeof token === 'string' ? token : '';
979
+ }
980
+ async postJson(url, body) {
981
+ const timeoutMs = this.config.timeoutMs ?? 15000;
982
+ const abortController = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
983
+ const timeoutId = abortController ? setTimeout(() => abortController.abort(), timeoutMs) : undefined;
984
+ try {
985
+ const response = await this.fetchFn(url, {
986
+ method: 'POST',
987
+ headers: {
988
+ 'Content-Type': 'application/json',
989
+ },
990
+ body: JSON.stringify(body),
991
+ signal: abortController?.signal,
992
+ });
993
+ if (!response.ok) {
994
+ const text = await response.text();
995
+ throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
435
996
  }
436
997
  return await response.json();
437
998
  }
@@ -442,6 +1003,94 @@ class FetchLicenseTransport {
442
1003
  }
443
1004
  }
444
1005
  }
1006
+ class DesignbaseAccountLicenseTransport {
1007
+ constructor(config) {
1008
+ this.config = config;
1009
+ this.client = new DesignbaseAccountAuthClient(config);
1010
+ this.fetchFn = config.fetchFn ?? fetch;
1011
+ }
1012
+ async verify(payload) {
1013
+ return this.validate(payload);
1014
+ }
1015
+ async validate(payload) {
1016
+ const token = await this.resolveAccessToken();
1017
+ if (!token) {
1018
+ return {
1019
+ valid: false,
1020
+ message: 'Designbase account access token is missing.',
1021
+ raw: { status: 'not_logged_in' },
1022
+ };
1023
+ }
1024
+ const url = new URL(joinUrl(this.config.baseUrl, this.config.statusPath || '/wp-json/custom/v1/product/status'));
1025
+ url.searchParams.set('plugin_code', payload.productId);
1026
+ const raw = await this.getJson(url.toString(), token);
1027
+ const status = readText(raw, ['status']);
1028
+ const allowed = Boolean(asRecord(raw).allowed || status === 'active');
1029
+ return {
1030
+ valid: allowed,
1031
+ message: readText(raw, ['message']) || undefined,
1032
+ productId: readText(raw, ['plugin_code', 'pluginCode']) || payload.productId,
1033
+ instanceId: payload.instanceId,
1034
+ activationLimit: readNumber(raw, ['activation_limit', 'activationLimit']) ?? 999999,
1035
+ activationUsage: readNumber(raw, ['activation_usage', 'activationUsage']) ?? 0,
1036
+ planTier: readText(raw, ['plan_tier', 'planTier']) || (allowed ? 'PRO' : 'FREE'),
1037
+ licenseKey: payload.licenseKey,
1038
+ raw,
1039
+ };
1040
+ }
1041
+ async deactivate(payload) {
1042
+ const token = await this.resolveAccessToken();
1043
+ if (!token) {
1044
+ return { success: true, message: 'Already signed out.' };
1045
+ }
1046
+ const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.deactivatePath || '/wp-json/custom/v1/product/deactivate'), {
1047
+ license_key: payload.licenseKey,
1048
+ device_id: payload.instanceId || payload.instanceName,
1049
+ plugin_code: payload.productId,
1050
+ }, token);
1051
+ const status = readText(raw, ['status']);
1052
+ return {
1053
+ success: Boolean(asRecord(raw).success || status === 'ok' || status === 'inactive'),
1054
+ message: readText(raw, ['message']) || undefined,
1055
+ raw,
1056
+ };
1057
+ }
1058
+ async resolveAccessToken() {
1059
+ if (this.config.getAccessToken) {
1060
+ const token = await this.config.getAccessToken();
1061
+ return typeof token === 'string' ? token : '';
1062
+ }
1063
+ return this.client.getAccessToken();
1064
+ }
1065
+ async getJson(url, token) {
1066
+ const response = await this.fetchFn(url, {
1067
+ method: 'GET',
1068
+ headers: {
1069
+ Authorization: `Bearer ${token}`,
1070
+ },
1071
+ });
1072
+ if (!response.ok) {
1073
+ const text = await response.text();
1074
+ throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
1075
+ }
1076
+ return response.json();
1077
+ }
1078
+ async postJson(url, body, token) {
1079
+ const response = await this.fetchFn(url, {
1080
+ method: 'POST',
1081
+ headers: {
1082
+ Authorization: `Bearer ${token}`,
1083
+ 'Content-Type': 'application/json',
1084
+ },
1085
+ body: JSON.stringify(body),
1086
+ });
1087
+ if (!response.ok) {
1088
+ const text = await response.text();
1089
+ throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
1090
+ }
1091
+ return response.json();
1092
+ }
1093
+ }
445
1094
 
446
1095
  class FigmaStorageAdapter {
447
1096
  constructor(figma) {
@@ -483,6 +1132,8 @@ const DEFAULT_MESSAGE_TYPES = {
483
1132
  class FigmaLicenseStatusManager {
484
1133
  constructor(options) {
485
1134
  this.options = options;
1135
+ this.featureAccessMessageType = 'feature-access-result';
1136
+ this.consumeUsageMessageType = 'usage-consume-result';
486
1137
  this.messageTypes = {
487
1138
  ...DEFAULT_MESSAGE_TYPES,
488
1139
  ...(options.messageTypes || {}),
@@ -490,7 +1141,16 @@ class FigmaLicenseStatusManager {
490
1141
  this.notifier = options.notifier;
491
1142
  }
492
1143
  async initialize() {
493
- await this.postStatusUpdate();
1144
+ if (!this.initializationPromise) {
1145
+ this.initializationPromise = (async () => {
1146
+ const state = await this.options.core.initialize();
1147
+ await this.postStatusUpdate(state);
1148
+ })().catch((error) => {
1149
+ this.initializationPromise = undefined;
1150
+ throw error;
1151
+ });
1152
+ }
1153
+ await this.initializationPromise;
494
1154
  }
495
1155
  async handleMessage(pluginMessage) {
496
1156
  if (!pluginMessage || typeof pluginMessage !== 'object') {
@@ -502,7 +1162,7 @@ class FigmaLicenseStatusManager {
502
1162
  return false;
503
1163
  }
504
1164
  if (type === this.messageTypes.initialize) {
505
- await this.postStatusUpdate();
1165
+ await this.initialize();
506
1166
  return true;
507
1167
  }
508
1168
  if (type === this.messageTypes.verifyLicense) {
@@ -514,6 +1174,17 @@ class FigmaLicenseStatusManager {
514
1174
  await this.handleDeactivateLicense();
515
1175
  return true;
516
1176
  }
1177
+ if (type === 'check-feature-access') {
1178
+ const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
1179
+ await this.handleCheckFeatureAccess(featureKey);
1180
+ return true;
1181
+ }
1182
+ if (type === 'consume-usage') {
1183
+ const amount = typeof message.amount === 'number' ? message.amount : 1;
1184
+ const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
1185
+ await this.handleConsumeUsage(amount, featureKey);
1186
+ return true;
1187
+ }
517
1188
  return false;
518
1189
  }
519
1190
  bindToOnMessage(fallbackHandler) {
@@ -529,14 +1200,19 @@ class FigmaLicenseStatusManager {
529
1200
  try {
530
1201
  const state = await this.options.core.verifyLicense(licenseKey);
531
1202
  const access = this.options.core.resolveUsageAccess(state);
1203
+ const summary = await this.options.core.getLicenseSummary();
532
1204
  this.postMessage({
533
1205
  type: this.messageTypes.verifyResult,
534
1206
  success: true,
535
1207
  message: '라이선스가 검증되었습니다.',
536
1208
  ...state,
1209
+ ...summary,
537
1210
  usageLimit: this.options.core.getUsageLimit(),
538
1211
  canUseFeature: access.allowed,
539
1212
  remainingUsage: access.remaining,
1213
+ usageUsed: access.used,
1214
+ usageReset: access.reset,
1215
+ usageTier: access.tier,
540
1216
  });
541
1217
  await this.postStatusUpdate(state);
542
1218
  }
@@ -549,20 +1225,26 @@ class FigmaLicenseStatusManager {
549
1225
  message: mappedError.message,
550
1226
  errorCode: mappedError.code,
551
1227
  });
1228
+ await this.postStatusUpdate();
552
1229
  }
553
1230
  }
554
1231
  async handleDeactivateLicense() {
555
1232
  try {
556
1233
  const state = await this.options.core.deactivateLicense();
557
1234
  const access = this.options.core.resolveUsageAccess(state);
1235
+ const summary = await this.options.core.getLicenseSummary();
558
1236
  this.postMessage({
559
1237
  type: this.messageTypes.deactivateResult,
560
1238
  success: true,
561
1239
  message: '라이선스가 비활성화되었습니다.',
562
1240
  ...state,
1241
+ ...summary,
563
1242
  usageLimit: this.options.core.getUsageLimit(),
564
1243
  canUseFeature: access.allowed,
565
1244
  remainingUsage: access.remaining,
1245
+ usageUsed: access.used,
1246
+ usageReset: access.reset,
1247
+ usageTier: access.tier,
566
1248
  });
567
1249
  await this.postStatusUpdate(state);
568
1250
  }
@@ -580,13 +1262,45 @@ class FigmaLicenseStatusManager {
580
1262
  async postStatusUpdate(initialState) {
581
1263
  const state = initialState ?? await this.options.core.getStatus();
582
1264
  const access = this.options.core.resolveUsageAccess(state);
1265
+ const summary = await this.options.core.getLicenseSummary();
583
1266
  this.postMessage({
584
1267
  type: this.messageTypes.updateStatus,
585
1268
  ...state,
1269
+ ...summary,
586
1270
  usageLimit: this.options.core.getUsageLimit(),
587
1271
  canUseFeature: access.allowed,
588
1272
  remainingUsage: access.remaining,
1273
+ usageUsed: access.used,
1274
+ usageReset: access.reset,
1275
+ usageTier: access.tier,
1276
+ usageReason: access.reason,
1277
+ });
1278
+ }
1279
+ async handleCheckFeatureAccess(featureKey) {
1280
+ const state = await this.options.core.getStatus();
1281
+ const access = this.options.core.resolveUsageAccess(state, featureKey);
1282
+ const summary = await this.options.core.getLicenseSummary();
1283
+ this.postMessage({
1284
+ type: this.featureAccessMessageType,
1285
+ success: true,
1286
+ featureKey,
1287
+ ...summary,
1288
+ ...access,
1289
+ });
1290
+ }
1291
+ async handleConsumeUsage(amount, featureKey) {
1292
+ const result = await this.options.core.consumeUsage(amount, featureKey);
1293
+ const summary = await this.options.core.getLicenseSummary();
1294
+ this.postMessage({
1295
+ type: this.consumeUsageMessageType,
1296
+ success: result.access.allowed,
1297
+ featureKey,
1298
+ amount,
1299
+ ...summary,
1300
+ ...result.access,
1301
+ usageCount: result.state.usageCount,
589
1302
  });
1303
+ await this.postStatusUpdate(result.state);
590
1304
  }
591
1305
  postMessage(message) {
592
1306
  this.options.figma.ui.postMessage(message);
@@ -614,6 +1328,7 @@ function createFigmaLicenseStatusManager(options) {
614
1328
  const notifier = options.notifier ?? new FigmaNotifierAdapter(figmaApi);
615
1329
  const transport = options.transport ?? new FetchLicenseTransport({
616
1330
  verifyUrl: options.config.endpoint,
1331
+ validateUrl: options.config.validateEndpoint,
617
1332
  deactivateUrl: options.config.deactivateEndpoint,
618
1333
  headers: options.config.requestHeaders,
619
1334
  timeoutMs: options.config.requestTimeoutMs,
@@ -633,5 +1348,5 @@ function createFigmaLicenseStatusManager(options) {
633
1348
  });
634
1349
  }
635
1350
 
636
- export { FetchLicenseTransport, FigmaLicenseStatusManager, FigmaNotifierAdapter, FigmaStorageAdapter, LicenseCore, LicenseCoreError, LicenseErrorCode, createFigmaLicenseStatusManager, defaultNormalizeDeactivateResponse, defaultNormalizeVerifyResponse, isActivationLimitReached, mapLicenseError };
1351
+ export { DesignbaseAccountAuthClient, DesignbaseAccountLicenseTransport, FetchLicenseTransport, FigmaLicenseStatusManager, FigmaNotifierAdapter, FigmaStorageAdapter, LicenseCore, LicenseCoreError, LicenseErrorCode, LicenseTransportHttpError, createFigmaLicenseStatusManager, defaultNormalizeDeactivateResponse, defaultNormalizeVerifyResponse, isActivationLimitReached, mapLicenseError };
637
1352
  //# sourceMappingURL=index.esm.js.map