@designbasekorea/figma-license-core 0.2.1 → 0.3.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/README.md CHANGED
@@ -8,7 +8,7 @@ Figma 플러그인의 라이선스 상태/사용량 정책/기능 제한을 공
8
8
  - `hasLicenseKey`: 라이선스 키가 저장되어 있는지
9
9
  - `isLicenseRegistered`: 실제 유효 등록(PAID + key) 상태인지
10
10
  - 티어별 사용량 정책
11
- - 일간 리셋 (`daily`), 월간 리셋 (`monthly`), 리셋 없음 (`none`)
11
+ - 일간 리셋 (`daily`), 주간 리셋 (`weekly`), 월간 리셋 (`monthly`), 리셋 없음 (`none`)
12
12
  - `limit: number | 'unlimited'`
13
13
  - 티어별 기능 제한
14
14
  - `allow`/`deny` 기반 기능 접근 제어
@@ -65,6 +65,32 @@ await statusManager.initialize();
65
65
  - `check-feature-access` (`featureKey` 선택)
66
66
  - `consume-usage` (`amount`, `featureKey` 선택)
67
67
 
68
+ `amount`가 남은 수량보다 크면 소비되지 않습니다. 마지막 1회를 소비한 응답도
69
+ `success: true`, `remaining: 0`으로 반환되므로 실행 성공 여부와 다음 차단 상태를 혼동하지 않습니다.
70
+
71
+ ## 플러그인 공통 사용 규칙
72
+
73
+ 모든 플러그인은 기능 실행 직전에 같은 순서를 사용합니다.
74
+
75
+ ```ts
76
+ const access = await statusManager.canUseFeature('bulk_apply', 1);
77
+ if (!access.allowed) {
78
+ // access.reason === 'FEATURE_NOT_ALLOWED' 또는 'USAGE_LIMIT_REACHED'
79
+ return;
80
+ }
81
+
82
+ const result = await statusManager.consumeUsage(1, 'bulk_apply');
83
+ if (!result.consumed) {
84
+ return;
85
+ }
86
+
87
+ await runBulkApply();
88
+ ```
89
+
90
+ UI 메시지를 사용하는 경우에는 `check-feature-access`로 미리 확인한 뒤
91
+ `consume-usage`를 실행 직전에 전송합니다. 서버에서 최종 권한을 판정하는 제품은
92
+ 동일한 feature key를 서버와 코어 설정에 사용해야 합니다.
93
+
68
94
  ### Main -> UI
69
95
 
70
96
  - `update-plugin-status`
@@ -88,6 +114,8 @@ await statusManager.initialize();
88
114
  - 운영 배포에서는 `masterCode`를 사용하지 마세요.
89
115
  - 최종 권한 판정은 서버 검증 결과 기준으로 처리하세요.
90
116
  - 클라이언트 상태(`paymentStatus`, `usageCount`)는 변조 가능하다고 가정하세요.
117
+ - 모든 플러그인의 리셋 단위·무료 한도·차단 기능은 `policy`에서만 선언하고,
118
+ 플러그인별 임의 카운터를 추가하지 마세요.
91
119
 
92
120
  ## 문서
93
121
 
@@ -32,6 +32,7 @@ export declare class FigmaNotifierAdapter implements LicenseNotifierAdapter {
32
32
  timeout?: number;
33
33
  }): void;
34
34
  }
35
+ export declare const DEFAULT_LICENSE_MESSAGE_TYPES: Required<LicenseManagerMessageTypes>;
35
36
  export interface FigmaLicenseStatusManagerOptions {
36
37
  figma: FigmaApiLike;
37
38
  core: LicenseCore;
@@ -46,13 +47,20 @@ export declare class FigmaLicenseStatusManager {
46
47
  private readonly options;
47
48
  private readonly messageTypes;
48
49
  private readonly notifier?;
49
- private readonly featureAccessMessageType;
50
- private readonly consumeUsageMessageType;
51
50
  private initializationPromise?;
52
51
  constructor(options: FigmaLicenseStatusManagerOptions);
53
52
  initialize(): Promise<void>;
54
53
  handleMessage(pluginMessage: unknown): Promise<boolean>;
55
54
  bindToOnMessage(fallbackHandler?: (pluginMessage: unknown) => void | Promise<void>): void;
55
+ /**
56
+ * Check a feature from main code using the same policy as the UI messages.
57
+ * This is the preferred guard for operations handled entirely in main.
58
+ */
59
+ canUseFeature(featureKey?: string, amount?: number): Promise<import("./types").UsageAccessResult>;
60
+ /**
61
+ * Consume usage from main code and publish the refreshed status to the UI.
62
+ */
63
+ consumeUsage(amount?: number, featureKey?: string): Promise<import("./types").UsageConsumptionResult>;
56
64
  private handleVerifyLicense;
57
65
  private handleDeactivateLicense;
58
66
  private postStatusUpdate;
@@ -1 +1 @@
1
- {"version":3,"file":"figmaStatusManager.d.ts","sourceRoot":"","sources":["../src/figmaStatusManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"figmaStatusManager.d.ts","sourceRoot":"","sources":["../src/figmaStatusManager.ts"],"names":[],"mappings":"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"}
package/dist/index.esm.js CHANGED
@@ -122,6 +122,7 @@ const STORAGE_KEYS = {
122
122
  activationUsage: 'activationUsage',
123
123
  usageCount: 'usageCount',
124
124
  previousUsageCount: 'previousUsageCount',
125
+ previousUsageWindowKey: 'previousUsageWindowKey',
125
126
  usageWindowKey: 'usageWindowKey',
126
127
  planTier: 'planTier',
127
128
  lastVerifiedAt: 'lastVerifiedAt',
@@ -186,7 +187,7 @@ function normalizeTier(tier) {
186
187
  return tier.trim() || 'FREE';
187
188
  }
188
189
  function normalizeUsageReset(reset) {
189
- if (reset === 'daily' || reset === 'monthly' || reset === 'none') {
190
+ if (reset === 'daily' || reset === 'weekly' || reset === 'monthly' || reset === 'none') {
190
191
  return reset;
191
192
  }
192
193
  return 'none';
@@ -201,6 +202,19 @@ function toDateKey(date, period) {
201
202
  if (period === 'monthly') {
202
203
  return `${year}-${month}`;
203
204
  }
205
+ if (period === 'weekly') {
206
+ // ISO weeks start on Monday and keep the week-year at the Thursday of
207
+ // the current week. Using UTC avoids a reset moving at local DST changes.
208
+ const dayOfWeek = date.getUTCDay() || 7;
209
+ const thursday = new Date(Date.UTC(year, date.getUTCMonth(), date.getUTCDate()));
210
+ thursday.setUTCDate(thursday.getUTCDate() + (4 - dayOfWeek));
211
+ const weekYear = thursday.getUTCFullYear();
212
+ const firstThursday = new Date(Date.UTC(weekYear, 0, 4));
213
+ const firstDay = firstThursday.getUTCDay() || 7;
214
+ firstThursday.setUTCDate(firstThursday.getUTCDate() + (4 - firstDay));
215
+ const week = 1 + Math.round((thursday.getTime() - firstThursday.getTime()) / 604800000);
216
+ return `${weekYear}-W${String(week).padStart(2, '0')}`;
217
+ }
204
218
  return `${year}-${month}-${day}`;
205
219
  }
206
220
  function normalizePolicy(config) {
@@ -239,6 +253,7 @@ class LicenseCore {
239
253
  constructor(config, deps) {
240
254
  this.config = config;
241
255
  this.deps = deps;
256
+ this.usageMutation = Promise.resolve();
242
257
  if (!config.productId.trim()) {
243
258
  throw new Error('LicenseConfig.productId is required.');
244
259
  }
@@ -283,13 +298,15 @@ class LicenseCore {
283
298
  }
284
299
  async resetLicenseState() {
285
300
  const current = await this.getStatus();
286
- const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
287
301
  const previousUsageCount = current.previousUsageCount ?? 0;
302
+ const previousUsageWindowKey = current.previousUsageWindowKey || '';
288
303
  const nextState = {
289
304
  ...current,
290
305
  paymentStatus: 'FREE',
291
306
  planTier: this.policy.freeTier,
292
- usageWindowKey: toDateKey(new Date(), freeReset),
307
+ // 유료 전환 전에 사용하던 무료 사용량 창을 복원한다. 날짜가 바뀌었다면
308
+ // applyUsageWindowResetIfNeeded()가 아래 카운트를 0으로 초기화한다.
309
+ usageWindowKey: previousUsageWindowKey,
293
310
  licenseKey: '',
294
311
  instanceId: '',
295
312
  activationLimit: 0,
@@ -309,6 +326,7 @@ class LicenseCore {
309
326
  const nextState = {
310
327
  ...current,
311
328
  paymentStatus: 'PAID',
329
+ planTier: normalizeTier(result.planTier || current.planTier || this.policy.paidTier),
312
330
  activationLimit: result.activationLimit ?? current.activationLimit,
313
331
  activationUsage: result.activationUsage ?? current.activationUsage,
314
332
  licenseStatus: result.licenseStatus || 'active',
@@ -322,12 +340,12 @@ class LicenseCore {
322
340
  return nextState;
323
341
  }
324
342
  async markValidationFailure(current, message, errorCode) {
325
- const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
343
+ const previousUsageWindowKey = current.previousUsageWindowKey || '';
326
344
  const nextState = {
327
345
  ...current,
328
346
  paymentStatus: 'UNPAID',
329
347
  planTier: this.policy.freeTier,
330
- usageWindowKey: toDateKey(new Date(), freeReset),
348
+ usageWindowKey: previousUsageWindowKey,
331
349
  activationLimit: 0,
332
350
  activationUsage: 0,
333
351
  usageCount: current.previousUsageCount ?? current.usageCount,
@@ -377,6 +395,7 @@ class LicenseCore {
377
395
  activationLimit: MASTER_CODE_ACTIVATION_LIMIT,
378
396
  activationUsage: 0,
379
397
  previousUsageCount: current.usageCount,
398
+ previousUsageWindowKey: current.usageWindowKey,
380
399
  lastVerifiedAt: new Date().toISOString(),
381
400
  licenseStatus: 'active',
382
401
  licenseSource: 'master',
@@ -424,6 +443,7 @@ class LicenseCore {
424
443
  activationLimit,
425
444
  activationUsage,
426
445
  previousUsageCount: current.usageCount,
446
+ previousUsageWindowKey: current.usageWindowKey,
427
447
  lastVerifiedAt: new Date().toISOString(),
428
448
  licenseStatus: verifyResult.licenseStatus || 'active',
429
449
  licenseSource: verifyResult.licenseSource,
@@ -466,7 +486,7 @@ class LicenseCore {
466
486
  if (current.paymentStatus === 'PAID') {
467
487
  return current;
468
488
  }
469
- const increasedAmount = Math.max(0, amount);
489
+ const increasedAmount = Number.isFinite(amount) ? Math.max(0, amount) : 0;
470
490
  if (increasedAmount === 0) {
471
491
  return current;
472
492
  }
@@ -477,28 +497,39 @@ class LicenseCore {
477
497
  await this.persistState(nextState);
478
498
  return nextState;
479
499
  }
480
- async canUseFeature(featureKey) {
500
+ async canUseFeature(featureKey, amount = 1) {
481
501
  const state = await this.getStatus();
482
- return this.resolveUsageAccess(state, featureKey);
502
+ return this.resolveUsageAccess(state, featureKey, amount);
483
503
  }
484
504
  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,
505
+ const execute = async () => {
506
+ const state = await this.getStatus();
507
+ const requestedAmount = Number.isFinite(amount) ? Math.max(0, amount) : 0;
508
+ const access = this.resolveUsageAccess(state, featureKey, requestedAmount);
509
+ if (!access.allowed || requestedAmount <= 0) {
510
+ return { state, access, consumed: false };
511
+ }
512
+ const updatedState = await this.incrementUsage(requestedAmount);
513
+ const updatedAccess = this.resolveUsageAccess(updatedState, featureKey, requestedAmount);
514
+ return {
515
+ state: updatedState,
516
+ access: updatedAccess,
517
+ consumed: true,
518
+ };
495
519
  };
496
- }
497
- resolveUsageAccess(state, featureKey) {
520
+ // Figma can deliver duplicate UI events while a panel remounts. Serialize
521
+ // the check + persist pair so two concurrent requests cannot overspend the
522
+ // same local allowance.
523
+ const pending = this.usageMutation.then(execute, execute);
524
+ this.usageMutation = pending.then(() => undefined, () => undefined);
525
+ return pending;
526
+ }
527
+ resolveUsageAccess(state, featureKey, amount = 1) {
498
528
  const tier = this.resolveTier(state);
499
529
  const usagePolicy = this.getUsagePolicy(tier);
500
530
  const featureAllowed = this.isFeatureAllowed(tier, featureKey);
501
531
  const isLicensed = this.isLicenseRegisteredState(state);
532
+ const requested = Number.isFinite(amount) && amount > 0 ? amount : 1;
502
533
  const used = state.usageCount;
503
534
  if (usagePolicy.limit === 'unlimited') {
504
535
  return {
@@ -507,6 +538,7 @@ class LicenseCore {
507
538
  used,
508
539
  limit: 'unlimited',
509
540
  reset: usagePolicy.reset,
541
+ requested,
510
542
  tier,
511
543
  hasLicenseKey: Boolean(state.licenseKey),
512
544
  isLicenseRegistered: isLicensed,
@@ -516,7 +548,7 @@ class LicenseCore {
516
548
  };
517
549
  }
518
550
  const remaining = Math.max(usagePolicy.limit - used, 0);
519
- const usageAllowed = remaining > 0;
551
+ const usageAllowed = remaining >= requested;
520
552
  const allowed = featureAllowed && usageAllowed;
521
553
  return {
522
554
  allowed,
@@ -524,6 +556,7 @@ class LicenseCore {
524
556
  used,
525
557
  limit: usagePolicy.limit,
526
558
  reset: usagePolicy.reset,
559
+ requested,
527
560
  tier,
528
561
  hasLicenseKey: Boolean(state.licenseKey),
529
562
  isLicenseRegistered: isLicensed,
@@ -532,19 +565,18 @@ class LicenseCore {
532
565
  reason: featureAllowed ? (usageAllowed ? undefined : 'USAGE_LIMIT_REACHED') : 'FEATURE_NOT_ALLOWED',
533
566
  };
534
567
  }
535
- getUsageLimit() {
536
- const freePolicy = this.getUsagePolicy(this.policy.freeTier);
537
- return freePolicy.limit === 'unlimited' ? Number.POSITIVE_INFINITY : freePolicy.limit;
568
+ getUsageLimit(state) {
569
+ const tier = state ? this.resolveTier(state) : this.policy.freeTier;
570
+ return this.getUsagePolicy(tier).limit;
538
571
  }
539
572
  getStoragePrefix() {
540
573
  return this.storagePrefix;
541
574
  }
542
575
  resolveTier(state) {
543
576
  const storedTier = normalizeTier(state.planTier || '');
544
- if (storedTier) {
577
+ {
545
578
  return storedTier;
546
579
  }
547
- return state.paymentStatus === 'PAID' ? this.policy.paidTier : this.policy.freeTier;
548
580
  }
549
581
  getUsagePolicy(tier) {
550
582
  return this.policy.usageByTier[tier]
@@ -579,7 +611,7 @@ class LicenseCore {
579
611
  }
580
612
  async loadState() {
581
613
  try {
582
- const [paymentStatus, licenseKey, instanceId, activationLimit, activationUsage, usageCount, previousUsageCount, usageWindowKey, planTier, lastVerifiedAt, licenseStatus, licenseSource, accessScope, expiresAt, validationMessage,] = await Promise.all([
614
+ const [paymentStatus, licenseKey, instanceId, activationLimit, activationUsage, usageCount, previousUsageCount, previousUsageWindowKey, usageWindowKey, planTier, lastVerifiedAt, licenseStatus, licenseSource, accessScope, expiresAt, validationMessage,] = await Promise.all([
583
615
  this.deps.storage.get(this.storageKey('paymentStatus')),
584
616
  this.deps.storage.get(this.storageKey('licenseKey')),
585
617
  this.deps.storage.get(this.storageKey('instanceId')),
@@ -587,6 +619,7 @@ class LicenseCore {
587
619
  this.deps.storage.get(this.storageKey('activationUsage')),
588
620
  this.deps.storage.get(this.storageKey('usageCount')),
589
621
  this.deps.storage.get(this.storageKey('previousUsageCount')),
622
+ this.deps.storage.get(this.storageKey('previousUsageWindowKey')),
590
623
  this.deps.storage.get(this.storageKey('usageWindowKey')),
591
624
  this.deps.storage.get(this.storageKey('planTier')),
592
625
  this.deps.storage.get(this.storageKey('lastVerifiedAt')),
@@ -608,6 +641,7 @@ class LicenseCore {
608
641
  activationUsage: toNonNegativeNumber(activationUsage, 0),
609
642
  usageCount: toNonNegativeNumber(usageCount, 0),
610
643
  previousUsageCount: toNonNegativeNumber(previousUsageCount, 0),
644
+ previousUsageWindowKey: toText(previousUsageWindowKey) || undefined,
611
645
  usageWindowKey: toText(usageWindowKey) || toDateKey(new Date(), usageReset),
612
646
  planTier: resolvedTier,
613
647
  lastVerifiedAt: toText(lastVerifiedAt) || undefined,
@@ -649,6 +683,7 @@ class LicenseCore {
649
683
  this.deps.storage.set(this.storageKey('activationUsage'), state.activationUsage),
650
684
  this.deps.storage.set(this.storageKey('usageCount'), state.usageCount),
651
685
  this.deps.storage.set(this.storageKey('previousUsageCount'), state.previousUsageCount || 0),
686
+ this.deps.storage.set(this.storageKey('previousUsageWindowKey'), state.previousUsageWindowKey || ''),
652
687
  this.deps.storage.set(this.storageKey('usageWindowKey'), state.usageWindowKey),
653
688
  this.deps.storage.set(this.storageKey('planTier'), state.planTier),
654
689
  this.deps.storage.set(this.storageKey('lastVerifiedAt'), state.lastVerifiedAt || ''),
@@ -1121,21 +1156,23 @@ class FigmaNotifierAdapter {
1121
1156
  }
1122
1157
  }
1123
1158
  }
1124
- const DEFAULT_MESSAGE_TYPES = {
1159
+ const DEFAULT_LICENSE_MESSAGE_TYPES = {
1125
1160
  initialize: 'initialize',
1126
1161
  verifyLicense: 'verify-license',
1127
1162
  deactivateLicense: 'deactivate-license',
1128
1163
  updateStatus: 'update-plugin-status',
1129
1164
  verifyResult: 'license-verification-result',
1130
1165
  deactivateResult: 'license-deactivation-result',
1166
+ checkFeatureAccess: 'check-feature-access',
1167
+ consumeUsage: 'consume-usage',
1168
+ featureAccessResult: 'feature-access-result',
1169
+ usageConsumeResult: 'usage-consume-result',
1131
1170
  };
1132
1171
  class FigmaLicenseStatusManager {
1133
1172
  constructor(options) {
1134
1173
  this.options = options;
1135
- this.featureAccessMessageType = 'feature-access-result';
1136
- this.consumeUsageMessageType = 'usage-consume-result';
1137
1174
  this.messageTypes = {
1138
- ...DEFAULT_MESSAGE_TYPES,
1175
+ ...DEFAULT_LICENSE_MESSAGE_TYPES,
1139
1176
  ...(options.messageTypes || {}),
1140
1177
  };
1141
1178
  this.notifier = options.notifier;
@@ -1174,13 +1211,18 @@ class FigmaLicenseStatusManager {
1174
1211
  await this.handleDeactivateLicense();
1175
1212
  return true;
1176
1213
  }
1177
- if (type === 'check-feature-access') {
1214
+ if (type === this.messageTypes.checkFeatureAccess) {
1178
1215
  const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
1179
- await this.handleCheckFeatureAccess(featureKey);
1216
+ const amount = typeof message.amount === 'number' && Number.isFinite(message.amount) && message.amount > 0
1217
+ ? message.amount
1218
+ : 1;
1219
+ await this.handleCheckFeatureAccess(featureKey, amount);
1180
1220
  return true;
1181
1221
  }
1182
- if (type === 'consume-usage') {
1183
- const amount = typeof message.amount === 'number' ? message.amount : 1;
1222
+ if (type === this.messageTypes.consumeUsage) {
1223
+ const amount = typeof message.amount === 'number' && Number.isFinite(message.amount) && message.amount > 0
1224
+ ? message.amount
1225
+ : 1;
1184
1226
  const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
1185
1227
  await this.handleConsumeUsage(amount, featureKey);
1186
1228
  return true;
@@ -1196,6 +1238,23 @@ class FigmaLicenseStatusManager {
1196
1238
  }
1197
1239
  };
1198
1240
  }
1241
+ /**
1242
+ * Check a feature from main code using the same policy as the UI messages.
1243
+ * This is the preferred guard for operations handled entirely in main.
1244
+ */
1245
+ async canUseFeature(featureKey, amount = 1) {
1246
+ const state = await this.options.core.getStatus();
1247
+ const safeAmount = Number.isFinite(amount) && amount > 0 ? amount : 1;
1248
+ return this.options.core.resolveUsageAccess(state, featureKey, safeAmount);
1249
+ }
1250
+ /**
1251
+ * Consume usage from main code and publish the refreshed status to the UI.
1252
+ */
1253
+ async consumeUsage(amount = 1, featureKey) {
1254
+ const result = await this.options.core.consumeUsage(amount, featureKey);
1255
+ await this.postStatusUpdate(result.state);
1256
+ return result;
1257
+ }
1199
1258
  async handleVerifyLicense(licenseKey) {
1200
1259
  try {
1201
1260
  const state = await this.options.core.verifyLicense(licenseKey);
@@ -1207,7 +1266,7 @@ class FigmaLicenseStatusManager {
1207
1266
  message: '라이선스가 검증되었습니다.',
1208
1267
  ...state,
1209
1268
  ...summary,
1210
- usageLimit: this.options.core.getUsageLimit(),
1269
+ usageLimit: this.options.core.getUsageLimit(state),
1211
1270
  canUseFeature: access.allowed,
1212
1271
  remainingUsage: access.remaining,
1213
1272
  usageUsed: access.used,
@@ -1239,7 +1298,7 @@ class FigmaLicenseStatusManager {
1239
1298
  message: '라이선스가 비활성화되었습니다.',
1240
1299
  ...state,
1241
1300
  ...summary,
1242
- usageLimit: this.options.core.getUsageLimit(),
1301
+ usageLimit: this.options.core.getUsageLimit(state),
1243
1302
  canUseFeature: access.allowed,
1244
1303
  remainingUsage: access.remaining,
1245
1304
  usageUsed: access.used,
@@ -1267,7 +1326,7 @@ class FigmaLicenseStatusManager {
1267
1326
  type: this.messageTypes.updateStatus,
1268
1327
  ...state,
1269
1328
  ...summary,
1270
- usageLimit: this.options.core.getUsageLimit(),
1329
+ usageLimit: this.options.core.getUsageLimit(state),
1271
1330
  canUseFeature: access.allowed,
1272
1331
  remainingUsage: access.remaining,
1273
1332
  usageUsed: access.used,
@@ -1276,14 +1335,15 @@ class FigmaLicenseStatusManager {
1276
1335
  usageReason: access.reason,
1277
1336
  });
1278
1337
  }
1279
- async handleCheckFeatureAccess(featureKey) {
1338
+ async handleCheckFeatureAccess(featureKey, amount = 1) {
1280
1339
  const state = await this.options.core.getStatus();
1281
- const access = this.options.core.resolveUsageAccess(state, featureKey);
1340
+ const access = this.options.core.resolveUsageAccess(state, featureKey, amount);
1282
1341
  const summary = await this.options.core.getLicenseSummary();
1283
1342
  this.postMessage({
1284
- type: this.featureAccessMessageType,
1343
+ type: this.messageTypes.featureAccessResult,
1285
1344
  success: true,
1286
1345
  featureKey,
1346
+ amount,
1287
1347
  ...summary,
1288
1348
  ...access,
1289
1349
  });
@@ -1292,10 +1352,11 @@ class FigmaLicenseStatusManager {
1292
1352
  const result = await this.options.core.consumeUsage(amount, featureKey);
1293
1353
  const summary = await this.options.core.getLicenseSummary();
1294
1354
  this.postMessage({
1295
- type: this.consumeUsageMessageType,
1296
- success: result.access.allowed,
1355
+ type: this.messageTypes.usageConsumeResult,
1356
+ success: result.consumed,
1297
1357
  featureKey,
1298
1358
  amount,
1359
+ consumed: result.consumed,
1299
1360
  ...summary,
1300
1361
  ...result.access,
1301
1362
  usageCount: result.state.usageCount,
@@ -1348,5 +1409,5 @@ function createFigmaLicenseStatusManager(options) {
1348
1409
  });
1349
1410
  }
1350
1411
 
1351
- export { DesignbaseAccountAuthClient, DesignbaseAccountLicenseTransport, FetchLicenseTransport, FigmaLicenseStatusManager, FigmaNotifierAdapter, FigmaStorageAdapter, LicenseCore, LicenseCoreError, LicenseErrorCode, LicenseTransportHttpError, createFigmaLicenseStatusManager, defaultNormalizeDeactivateResponse, defaultNormalizeVerifyResponse, isActivationLimitReached, mapLicenseError };
1412
+ export { DEFAULT_LICENSE_MESSAGE_TYPES, DesignbaseAccountAuthClient, DesignbaseAccountLicenseTransport, FetchLicenseTransport, FigmaLicenseStatusManager, FigmaNotifierAdapter, FigmaStorageAdapter, LicenseCore, LicenseCoreError, LicenseErrorCode, LicenseTransportHttpError, createFigmaLicenseStatusManager, defaultNormalizeDeactivateResponse, defaultNormalizeVerifyResponse, isActivationLimitReached, mapLicenseError };
1352
1413
  //# sourceMappingURL=index.esm.js.map