@designbasekorea/figma-license-core 0.3.3 → 0.4.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
@@ -298,7 +298,7 @@
298
298
  }
299
299
  async initialize() {
300
300
  let state = await this.getStatus();
301
- if (state.paymentStatus === 'PAID' && state.licenseKey && state.licenseKey !== this.config.masterCode) {
301
+ if (state.licenseKey && state.licenseKey !== this.config.masterCode) {
302
302
  try {
303
303
  const validateResult = this.deps.transport.validate
304
304
  ? await this.deps.transport.validate({
@@ -439,10 +439,12 @@
439
439
  }
440
440
  let verifyResult;
441
441
  try {
442
+ const current = await this.getStatus();
442
443
  verifyResult = await this.deps.transport.verify({
443
444
  licenseKey: trimmedLicenseKey,
444
445
  productId: this.config.productId,
445
446
  instanceName: this.config.instanceName,
447
+ instanceId: current.licenseKey === trimmedLicenseKey ? current.instanceId : undefined,
446
448
  });
447
449
  }
448
450
  catch (error) {
@@ -458,7 +460,9 @@
458
460
  }
459
461
  const activationLimit = toNonNegativeNumber(verifyResult.activationLimit, 0);
460
462
  const activationUsage = toNonNegativeNumber(verifyResult.activationUsage, 0);
461
- if (isActivationLimitReached(activationLimit, activationUsage)) {
463
+ // A verified instance already occupies its seat. usage === limit is not
464
+ // an error after successful activation (e.g. the first device on a 1-seat key).
465
+ if (!verifyResult.instanceId && isActivationLimitReached(activationLimit, activationUsage)) {
462
466
  throw new LicenseCoreError(exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED, '활성화 가능한 기기 수를 모두 사용했습니다. 기존 기기를 비활성화한 뒤 다시 시도해 주세요.', verifyResult);
463
467
  }
464
468
  const current = await this.getStatus();
@@ -821,9 +825,12 @@
821
825
  const statusValid = typeof licenseStatus === 'string'
822
826
  ? licenseStatus.toLowerCase() === 'active'
823
827
  : undefined;
824
- const valid = readBooleanField(body, ['valid', 'success', 'active'])
828
+ const rejectedStatus = ['expired', 'revoked', 'invalid', 'inactive', 'not_started',
829
+ 'product_not_allowed', 'rate_limited', 'activation_limit_reached', 'environment_mismatch', 'mode_unverified']
830
+ .includes(licenseStatus?.trim().toLowerCase() || '');
831
+ const valid = !rejectedStatus && (readBooleanField(body, ['valid', 'success', 'active'])
825
832
  ?? statusValid
826
- ?? false;
833
+ ?? false);
827
834
  const normalizedStatus = licenseStatus?.trim().toLowerCase();
828
835
  const fallbackMessage = !valid ? failureMessageForStatus(normalizedStatus) : undefined;
829
836
  const message = readTextField(body, ['message', 'error', 'detail']) || fallbackMessage;
@@ -1198,6 +1205,205 @@
1198
1205
  }
1199
1206
  }
1200
1207
 
1208
+ /** Copy legacy storage once. Paid status is revalidated before any UI is published. */
1209
+ async function migrateLegacyLicenseStorage(storage, prefix, keys = {}) {
1210
+ const oldKey = (field) => {
1211
+ const oldPrefix = keys.sourcePrefix ? `${keys.sourcePrefix}:` : '';
1212
+ return `${oldPrefix}${keys[field] || field}`;
1213
+ };
1214
+ const marker = `${prefix}:legacyMigration`;
1215
+ if (await storage.get(marker))
1216
+ return;
1217
+ const existing = await storage.get(`${prefix}:paymentStatus`);
1218
+ if (existing === undefined || existing === null) {
1219
+ const key = await storage.get(oldKey('licenseKey'));
1220
+ const instanceId = await storage.get(oldKey('instanceId'));
1221
+ const paymentStatus = await storage.get(oldKey('paymentStatus'));
1222
+ const usage = await storage.get(oldKey('usageCount'));
1223
+ if (typeof key === 'string' && key.trim()) {
1224
+ await storage.set(`${prefix}:licenseKey`, key.trim());
1225
+ await storage.set(`${prefix}:instanceId`, typeof instanceId === 'string' ? instanceId : '');
1226
+ await storage.set(`${prefix}:planTier`, paymentStatus === 'PAID' ? 'PAID' : 'FREE');
1227
+ }
1228
+ if (typeof usage === 'number' && Number.isFinite(usage) && usage >= 0) {
1229
+ await storage.set(`${prefix}:usageCount`, Math.floor(usage));
1230
+ }
1231
+ if (typeof key === 'string' && key.trim()) {
1232
+ await storage.set(`${prefix}:paymentStatus`, paymentStatus === 'PAID' ? 'PAID' : 'FREE');
1233
+ }
1234
+ }
1235
+ // Do not delete legacy keys: another version may still need them for rollback.
1236
+ await storage.set(marker, 1);
1237
+ }
1238
+ async function readOrCreateDeviceId(storage, prefix) {
1239
+ const storageKey = `${prefix}:deviceId`;
1240
+ const current = await storage.get(storageKey);
1241
+ if (typeof current === 'string' && current.length > 0)
1242
+ return current;
1243
+ const legacy = await storage.get('licenseDeviceId');
1244
+ const nonce = `${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
1245
+ const device = typeof legacy === 'string' && legacy.length > 0
1246
+ ? legacy
1247
+ : `license-${Date.now().toString(36)}-${nonce}`;
1248
+ await storage.set(storageKey, device);
1249
+ return device;
1250
+ }
1251
+ const pendingDeviceIds = new WeakMap();
1252
+ async function getPersistentLicenseDeviceId(storage, prefix) {
1253
+ let pending = pendingDeviceIds.get(storage);
1254
+ if (!pending) {
1255
+ pending = new Map();
1256
+ pendingDeviceIds.set(storage, pending);
1257
+ }
1258
+ const existing = pending.get(prefix);
1259
+ if (existing)
1260
+ return existing;
1261
+ const request = readOrCreateDeviceId(storage, prefix);
1262
+ pending.set(prefix, request);
1263
+ try {
1264
+ return await request;
1265
+ }
1266
+ finally {
1267
+ pending.delete(prefix);
1268
+ }
1269
+ }
1270
+
1271
+ const errors = {
1272
+ expired: [
1273
+ exports.LicenseErrorCode.LICENSE_EXPIRED,
1274
+ '라이선스가 만료되었습니다. 구매 내역 또는 이번 달 멤버십 키를 확인해 주세요.',
1275
+ ],
1276
+ revoked: [exports.LicenseErrorCode.LICENSE_REVOKED, '사용 중지된 라이선스입니다.'],
1277
+ not_started: [
1278
+ exports.LicenseErrorCode.INVALID_LICENSE,
1279
+ '라이선스 사용 기간이 아직 시작되지 않았습니다.',
1280
+ ],
1281
+ product_not_allowed: [
1282
+ exports.LicenseErrorCode.PRODUCT_MISMATCH,
1283
+ '이 라이선스는 현재 플러그인 구매 키가 아닙니다.',
1284
+ ],
1285
+ environment_mismatch: [
1286
+ exports.LicenseErrorCode.INVALID_LICENSE,
1287
+ '테스트 구매 키는 테스트 인증 서버에서만 사용할 수 있습니다.',
1288
+ ],
1289
+ mode_unverified: [
1290
+ exports.LicenseErrorCode.NETWORK_ERROR,
1291
+ '라이선스의 테스트·운영 구분을 확인하지 못했습니다.',
1292
+ ],
1293
+ rate_limited: [
1294
+ exports.LicenseErrorCode.RATE_LIMITED,
1295
+ '인증 요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.',
1296
+ ],
1297
+ network_error: [
1298
+ exports.LicenseErrorCode.NETWORK_ERROR,
1299
+ '라이선스 서버에 연결하지 못했습니다. 다시 시도해 주세요.',
1300
+ ],
1301
+ activation_limit_reached: [
1302
+ exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED,
1303
+ '사용 가능한 기기 수를 모두 사용했습니다. 다른 기기에서 해제해 주세요.',
1304
+ ],
1305
+ activation_required: [exports.LicenseErrorCode.INVALID_LICENSE, '라이선스를 다시 인증해 주세요.'],
1306
+ invalid_instance: [
1307
+ exports.LicenseErrorCode.INVALID_LICENSE,
1308
+ '이 기기의 인증 정보가 만료되었습니다. 다시 인증해 주세요.',
1309
+ ],
1310
+ };
1311
+ function normalizeWordPressLicenseResponse(raw, productId) {
1312
+ const data = raw && typeof raw === 'object' ? raw : {};
1313
+ const status = typeof data.status === 'string' ? data.status : 'invalid';
1314
+ if (data.valid !== true || status !== 'active') {
1315
+ const [code, message] = errors[status] || [
1316
+ exports.LicenseErrorCode.INVALID_LICENSE,
1317
+ '유효하지 않은 라이선스 키입니다.',
1318
+ ];
1319
+ throw new LicenseCoreError(code, message, undefined, code === exports.LicenseErrorCode.NETWORK_ERROR || code === exports.LicenseErrorCode.RATE_LIMITED);
1320
+ }
1321
+ if (data.product_id !== productId) {
1322
+ throw new LicenseCoreError(exports.LicenseErrorCode.PRODUCT_MISMATCH, '인증 서버의 상품 정보가 일치하지 않습니다.');
1323
+ }
1324
+ const source = typeof data.source === 'string' ? data.source : '';
1325
+ if (data.provider === 'lemon_api' &&
1326
+ (typeof data.instance_id !== 'string' || !data.instance_id)) {
1327
+ throw new LicenseCoreError(exports.LicenseErrorCode.INVALID_LICENSE, '기기 활성화 정보를 확인하지 못했습니다.');
1328
+ }
1329
+ return {
1330
+ valid: true,
1331
+ productId,
1332
+ planTier: 'PAID',
1333
+ licenseStatus: 'active',
1334
+ instanceId: typeof data.instance_id === 'string' ? data.instance_id : undefined,
1335
+ activationLimit: typeof data.activation_limit === 'number' ? data.activation_limit : 0,
1336
+ activationUsage: typeof data.activation_usage === 'number' ? data.activation_usage : 0,
1337
+ licenseSource: source,
1338
+ accessScope: typeof data.access_scope === 'string' ? data.access_scope : 'product',
1339
+ expiresAt: typeof data.expires_at === 'string' ? data.expires_at : undefined,
1340
+ };
1341
+ }
1342
+ function createWordPressLicenseTransport(options) {
1343
+ const { storage, storagePrefix: prefix, productId } = options;
1344
+ if (!productId.trim() || !prefix.trim())
1345
+ throw new Error('productId and storagePrefix are required.');
1346
+ if (!/^https:\/\//i.test(options.baseUrl) &&
1347
+ !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i.test(options.baseUrl)) {
1348
+ throw new Error('License server must use HTTPS (HTTP is allowed only for local development).');
1349
+ }
1350
+ const timeout = options.timeoutMs || 20000;
1351
+ const fetchFn = options.fetchFn || fetch;
1352
+ const body = (payload) => ({
1353
+ license_key: payload.licenseKey,
1354
+ product_id: productId,
1355
+ instance_id: payload.instanceId || '',
1356
+ device_id: payload.deviceId,
1357
+ });
1358
+ const base = options.baseUrl.replace(/\/$/, '');
1359
+ const http = new FetchLicenseTransport({
1360
+ verifyUrl: `${base}/activate`,
1361
+ validateUrl: `${base}/validate`,
1362
+ deactivateUrl: `${base}/deactivate`,
1363
+ fetchFn,
1364
+ useAbortSignal: false,
1365
+ timeoutMs: timeout,
1366
+ buildVerifyBody: body,
1367
+ buildValidateBody: body,
1368
+ buildDeactivateBody: body,
1369
+ normalizeVerifyResponse: (raw) => normalizeWordPressLicenseResponse(raw, productId),
1370
+ normalizeDeactivateResponse: (raw) => {
1371
+ const data = raw && typeof raw === 'object' ? raw : {};
1372
+ const success = data.success === true || data.deactivated === true;
1373
+ return {
1374
+ success,
1375
+ message: success
1376
+ ? undefined
1377
+ : '라이선스 연결을 해제하지 못했습니다. 다시 시도해 주세요.',
1378
+ };
1379
+ },
1380
+ });
1381
+ const enrich = async (payload) => ({
1382
+ ...payload,
1383
+ deviceId: await getPersistentLicenseDeviceId(storage, prefix),
1384
+ });
1385
+ return {
1386
+ verify: async (payload) => {
1387
+ const storedKey = await storage.get(`${prefix}:licenseKey`);
1388
+ const storedInstance = await storage.get(`${prefix}:instanceId`);
1389
+ return http.verify(await enrich({
1390
+ ...payload,
1391
+ instanceId: payload.instanceId ||
1392
+ (storedKey === payload.licenseKey && typeof storedInstance === 'string'
1393
+ ? storedInstance
1394
+ : ''),
1395
+ }));
1396
+ },
1397
+ validate: async (payload) => {
1398
+ const enriched = await enrich(payload);
1399
+ // Legacy users may not have a Lemon instance yet. Activate once;
1400
+ // the server reuses its device mapping when a response was lost.
1401
+ return payload.instanceId ? http.validate(enriched) : http.verify(enriched);
1402
+ },
1403
+ deactivate: async (payload) => http.deactivate(await enrich(payload)),
1404
+ };
1405
+ }
1406
+
1201
1407
  class FigmaStorageAdapter {
1202
1408
  constructor(figma) {
1203
1409
  this.figma = figma;
@@ -1242,6 +1448,7 @@
1242
1448
  class FigmaLicenseStatusManager {
1243
1449
  constructor(options) {
1244
1450
  this.options = options;
1451
+ this.messageQueue = Promise.resolve();
1245
1452
  this.messageTypes = {
1246
1453
  ...DEFAULT_LICENSE_MESSAGE_TYPES,
1247
1454
  ...(options.messageTypes || {}),
@@ -1251,6 +1458,7 @@
1251
1458
  async initialize() {
1252
1459
  if (!this.initializationPromise) {
1253
1460
  this.initializationPromise = (async () => {
1461
+ await this.options.beforeInitialize?.();
1254
1462
  const state = await this.options.core.initialize();
1255
1463
  await this.postStatusUpdate(state);
1256
1464
  })().catch((error) => {
@@ -1260,7 +1468,12 @@
1260
1468
  }
1261
1469
  await this.initializationPromise;
1262
1470
  }
1263
- async handleMessage(pluginMessage) {
1471
+ handleMessage(pluginMessage) {
1472
+ const operation = this.messageQueue.then(() => this.handleMessageNow(pluginMessage));
1473
+ this.messageQueue = operation.then(() => undefined, () => undefined);
1474
+ return operation;
1475
+ }
1476
+ async handleMessageNow(pluginMessage) {
1264
1477
  if (!pluginMessage || typeof pluginMessage !== 'object') {
1265
1478
  return false;
1266
1479
  }
@@ -1274,15 +1487,18 @@
1274
1487
  return true;
1275
1488
  }
1276
1489
  if (type === this.messageTypes.verifyLicense) {
1490
+ await this.initialize();
1277
1491
  const licenseKey = typeof message.licenseKey === 'string' ? message.licenseKey : '';
1278
1492
  await this.handleVerifyLicense(licenseKey);
1279
1493
  return true;
1280
1494
  }
1281
1495
  if (type === this.messageTypes.deactivateLicense) {
1496
+ await this.initialize();
1282
1497
  await this.handleDeactivateLicense();
1283
1498
  return true;
1284
1499
  }
1285
1500
  if (type === this.messageTypes.checkFeatureAccess) {
1501
+ await this.initialize();
1286
1502
  const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
1287
1503
  const amount = typeof message.amount === 'number' && Number.isFinite(message.amount) && message.amount > 0
1288
1504
  ? message.amount
@@ -1291,6 +1507,7 @@
1291
1507
  return true;
1292
1508
  }
1293
1509
  if (type === this.messageTypes.consumeUsage) {
1510
+ await this.initialize();
1294
1511
  const amount = typeof message.amount === 'number' && Number.isFinite(message.amount) && message.amount > 0
1295
1512
  ? message.amount
1296
1513
  : 1;
@@ -1314,6 +1531,7 @@
1314
1531
  * This is the preferred guard for operations handled entirely in main.
1315
1532
  */
1316
1533
  async canUseFeature(featureKey, amount = 1) {
1534
+ await this.initialize();
1317
1535
  const state = await this.options.core.getStatus();
1318
1536
  const safeAmount = Number.isFinite(amount) && amount > 0 ? amount : 1;
1319
1537
  return this.options.core.resolveUsageAccess(state, featureKey, safeAmount);
@@ -1322,6 +1540,7 @@
1322
1540
  * Consume usage from main code and publish the refreshed status to the UI.
1323
1541
  */
1324
1542
  async consumeUsage(amount = 1, featureKey) {
1543
+ await this.initialize();
1325
1544
  const result = await this.options.core.consumeUsage(amount, featureKey);
1326
1545
  await this.postStatusUpdate(result.state);
1327
1546
  return result;
@@ -1458,14 +1677,21 @@
1458
1677
  const figmaApi = resolveFigmaApi(options.figma);
1459
1678
  const storage = options.storage ?? new FigmaStorageAdapter(figmaApi);
1460
1679
  const notifier = options.notifier ?? new FigmaNotifierAdapter(figmaApi);
1461
- const transport = options.transport ?? new FetchLicenseTransport({
1462
- verifyUrl: options.config.endpoint,
1463
- validateUrl: options.config.validateEndpoint,
1464
- deactivateUrl: options.config.deactivateEndpoint,
1465
- headers: options.config.requestHeaders,
1466
- timeoutMs: options.config.requestTimeoutMs,
1467
- fetchFn: options.fetchFn,
1468
- });
1680
+ const prefix = options.config.storagePrefix?.trim() || `${options.config.instanceName}:${options.config.productId}`;
1681
+ const transport = options.transport ?? (options.wordpress
1682
+ ? createWordPressLicenseTransport({
1683
+ baseUrl: options.wordpress.baseUrl, productId: options.config.productId,
1684
+ storagePrefix: prefix, storage, fetchFn: options.fetchFn,
1685
+ timeoutMs: options.config.requestTimeoutMs,
1686
+ })
1687
+ : new FetchLicenseTransport({
1688
+ verifyUrl: options.config.endpoint,
1689
+ validateUrl: options.config.validateEndpoint,
1690
+ deactivateUrl: options.config.deactivateEndpoint,
1691
+ headers: options.config.requestHeaders,
1692
+ timeoutMs: options.config.requestTimeoutMs,
1693
+ fetchFn: options.fetchFn,
1694
+ }));
1469
1695
  const coreDependencies = {
1470
1696
  storage,
1471
1697
  transport,
@@ -1477,6 +1703,26 @@
1477
1703
  core,
1478
1704
  notifier,
1479
1705
  messageTypes: options.messageTypes,
1706
+ beforeInitialize: options.migrateLegacyStorage
1707
+ ? () => migrateLegacyLicenseStorage(storage, prefix, typeof options.migrateLegacyStorage === 'object' ? options.migrateLegacyStorage : undefined)
1708
+ : undefined,
1709
+ });
1710
+ }
1711
+ /** Shared membership + Lemon gateway setup. Product allowlists stay on the server. */
1712
+ function createWordPressFigmaLicenseManager(options) {
1713
+ const base = options.serverBaseUrl.replace(/\/$/, '');
1714
+ return createFigmaLicenseStatusManager({
1715
+ figma: options.figma, storage: options.storage, notifier: options.notifier,
1716
+ fetchFn: options.fetchFn, messageTypes: options.messageTypes,
1717
+ migrateLegacyStorage: options.migrateLegacyStorage ?? true,
1718
+ wordpress: { baseUrl: base },
1719
+ config: {
1720
+ productId: options.productId, instanceName: options.instanceName || options.productId,
1721
+ storagePrefix: options.storagePrefix || `designbase:${options.productId}`,
1722
+ endpoint: `${base}/activate`, validateEndpoint: `${base}/validate`, deactivateEndpoint: `${base}/deactivate`,
1723
+ requestTimeoutMs: options.timeoutMs || 20000,
1724
+ policy: options.policy,
1725
+ },
1480
1726
  });
1481
1727
  }
1482
1728
 
@@ -1491,10 +1737,15 @@
1491
1737
  exports.LicenseCoreError = LicenseCoreError;
1492
1738
  exports.LicenseTransportHttpError = LicenseTransportHttpError;
1493
1739
  exports.createFigmaLicenseStatusManager = createFigmaLicenseStatusManager;
1740
+ exports.createWordPressFigmaLicenseManager = createWordPressFigmaLicenseManager;
1741
+ exports.createWordPressLicenseTransport = createWordPressLicenseTransport;
1494
1742
  exports.defaultNormalizeDeactivateResponse = defaultNormalizeDeactivateResponse;
1495
1743
  exports.defaultNormalizeVerifyResponse = defaultNormalizeVerifyResponse;
1744
+ exports.getPersistentLicenseDeviceId = getPersistentLicenseDeviceId;
1496
1745
  exports.isActivationLimitReached = isActivationLimitReached;
1497
1746
  exports.mapLicenseError = mapLicenseError;
1747
+ exports.migrateLegacyLicenseStorage = migrateLegacyLicenseStorage;
1748
+ exports.normalizeWordPressLicenseResponse = normalizeWordPressLicenseResponse;
1498
1749
 
1499
1750
  }));
1500
1751
  //# sourceMappingURL=index.umd.js.map