@designbasekorea/figma-license-core 0.1.0 → 0.3.0

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