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