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