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