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