@designbasekorea/figma-license-core 0.1.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/README.md +59 -0
- package/dist/fetchLicenseTransport.d.ts +23 -0
- package/dist/fetchLicenseTransport.d.ts.map +1 -0
- package/dist/figmaStatusManager.d.ts +69 -0
- package/dist/figmaStatusManager.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +637 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +649 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +655 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/licenseCore.d.ts +27 -0
- package/dist/licenseCore.d.ts.map +1 -0
- package/dist/licenseErrorMapper.d.ts +4 -0
- package/dist/licenseErrorMapper.d.ts.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
exports.LicenseErrorCode = void 0;
|
|
4
|
+
(function (LicenseErrorCode) {
|
|
5
|
+
LicenseErrorCode["ACTIVATION_LIMIT_REACHED"] = "ACTIVATION_LIMIT_REACHED";
|
|
6
|
+
LicenseErrorCode["INVALID_LICENSE"] = "INVALID_LICENSE";
|
|
7
|
+
LicenseErrorCode["PRODUCT_MISMATCH"] = "PRODUCT_MISMATCH";
|
|
8
|
+
LicenseErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
9
|
+
LicenseErrorCode["STORAGE_ERROR"] = "STORAGE_ERROR";
|
|
10
|
+
LicenseErrorCode["SERVER_ERROR"] = "SERVER_ERROR";
|
|
11
|
+
LicenseErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
12
|
+
})(exports.LicenseErrorCode || (exports.LicenseErrorCode = {}));
|
|
13
|
+
class LicenseCoreError extends Error {
|
|
14
|
+
constructor(code, message, details, retriable = false) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.details = details;
|
|
18
|
+
this.retriable = retriable;
|
|
19
|
+
this.name = 'LicenseCoreError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function asText(input) {
|
|
24
|
+
if (!input) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
if (typeof input === 'string') {
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
30
|
+
if (typeof input === 'object' && 'message' in input) {
|
|
31
|
+
const maybeMessage = input.message;
|
|
32
|
+
if (typeof maybeMessage === 'string') {
|
|
33
|
+
return maybeMessage;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return JSON.stringify(input);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return String(input);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function inferCode(text) {
|
|
44
|
+
const normalized = text.toLowerCase();
|
|
45
|
+
if (normalized.includes('activation limit')
|
|
46
|
+
|| normalized.includes('seat')
|
|
47
|
+
|| normalized.includes('too many activations')
|
|
48
|
+
|| normalized.includes('already activated on maximum')) {
|
|
49
|
+
return exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED;
|
|
50
|
+
}
|
|
51
|
+
if (normalized.includes('invalid license')
|
|
52
|
+
|| normalized.includes('not found')
|
|
53
|
+
|| normalized.includes('not valid')
|
|
54
|
+
|| normalized.includes('license key is invalid')) {
|
|
55
|
+
return exports.LicenseErrorCode.INVALID_LICENSE;
|
|
56
|
+
}
|
|
57
|
+
if (normalized.includes('product mismatch') || normalized.includes('wrong product')) {
|
|
58
|
+
return exports.LicenseErrorCode.PRODUCT_MISMATCH;
|
|
59
|
+
}
|
|
60
|
+
if (normalized.includes('network')
|
|
61
|
+
|| normalized.includes('fetch')
|
|
62
|
+
|| normalized.includes('timeout')
|
|
63
|
+
|| normalized.includes('failed to connect')) {
|
|
64
|
+
return exports.LicenseErrorCode.NETWORK_ERROR;
|
|
65
|
+
}
|
|
66
|
+
if (normalized.includes('500') || normalized.includes('502') || normalized.includes('503')) {
|
|
67
|
+
return exports.LicenseErrorCode.SERVER_ERROR;
|
|
68
|
+
}
|
|
69
|
+
return exports.LicenseErrorCode.UNKNOWN_ERROR;
|
|
70
|
+
}
|
|
71
|
+
function mapLicenseError(error, fallbackMessage = '라이선스 처리 중 알 수 없는 오류가 발생했습니다.') {
|
|
72
|
+
if (error instanceof LicenseCoreError) {
|
|
73
|
+
return error;
|
|
74
|
+
}
|
|
75
|
+
const text = asText(error);
|
|
76
|
+
const message = text.trim() || fallbackMessage;
|
|
77
|
+
const code = inferCode(message);
|
|
78
|
+
return new LicenseCoreError(code, message, error, code === exports.LicenseErrorCode.NETWORK_ERROR || code === exports.LicenseErrorCode.SERVER_ERROR);
|
|
79
|
+
}
|
|
80
|
+
function isActivationLimitReached(activationLimit, activationUsage) {
|
|
81
|
+
if (activationLimit <= 0) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return activationUsage >= activationLimit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const STORAGE_KEYS = {
|
|
88
|
+
paymentStatus: 'paymentStatus',
|
|
89
|
+
licenseKey: 'licenseKey',
|
|
90
|
+
activationLimit: 'activationLimit',
|
|
91
|
+
activationUsage: 'activationUsage',
|
|
92
|
+
usageCount: 'usageCount',
|
|
93
|
+
lastVerifiedAt: 'lastVerifiedAt',
|
|
94
|
+
};
|
|
95
|
+
const DEFAULT_USAGE_LIMIT = 20;
|
|
96
|
+
const MASTER_CODE_ACTIVATION_LIMIT = 999999;
|
|
97
|
+
function toNonNegativeNumber(value, fallback = 0) {
|
|
98
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
102
|
+
const numeric = Number(value);
|
|
103
|
+
if (Number.isFinite(numeric) && numeric >= 0) {
|
|
104
|
+
return numeric;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
function toText(value) {
|
|
110
|
+
return typeof value === 'string' ? value : '';
|
|
111
|
+
}
|
|
112
|
+
function toPaymentStatus(value) {
|
|
113
|
+
if (value === 'PAID' || value === 'UNPAID' || value === 'FREE') {
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
return 'FREE';
|
|
117
|
+
}
|
|
118
|
+
function normalizeProductId(productId) {
|
|
119
|
+
return productId.trim().toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
class LicenseCore {
|
|
122
|
+
constructor(config, deps) {
|
|
123
|
+
this.config = config;
|
|
124
|
+
this.deps = deps;
|
|
125
|
+
if (!config.productId.trim()) {
|
|
126
|
+
throw new Error('LicenseConfig.productId is required.');
|
|
127
|
+
}
|
|
128
|
+
if (!config.endpoint.trim()) {
|
|
129
|
+
throw new Error('LicenseConfig.endpoint is required.');
|
|
130
|
+
}
|
|
131
|
+
if (!config.instanceName.trim()) {
|
|
132
|
+
throw new Error('LicenseConfig.instanceName is required.');
|
|
133
|
+
}
|
|
134
|
+
this.usageLimit = Math.max(config.usageLimit ?? DEFAULT_USAGE_LIMIT, 0);
|
|
135
|
+
this.storagePrefix = config.storagePrefix?.trim() || `${config.instanceName}:${config.productId}`;
|
|
136
|
+
}
|
|
137
|
+
async initialize() {
|
|
138
|
+
return this.loadState();
|
|
139
|
+
}
|
|
140
|
+
async getStatus() {
|
|
141
|
+
return this.loadState();
|
|
142
|
+
}
|
|
143
|
+
async verifyLicense(licenseKey) {
|
|
144
|
+
const trimmedLicenseKey = licenseKey.trim();
|
|
145
|
+
if (!trimmedLicenseKey) {
|
|
146
|
+
throw new LicenseCoreError(exports.LicenseErrorCode.INVALID_LICENSE, '라이선스 키를 입력해 주세요.');
|
|
147
|
+
}
|
|
148
|
+
if (this.config.masterCode && trimmedLicenseKey === this.config.masterCode) {
|
|
149
|
+
const current = await this.loadState();
|
|
150
|
+
const nextState = {
|
|
151
|
+
...current,
|
|
152
|
+
paymentStatus: 'PAID',
|
|
153
|
+
licenseKey: trimmedLicenseKey,
|
|
154
|
+
activationLimit: MASTER_CODE_ACTIVATION_LIMIT,
|
|
155
|
+
activationUsage: 0,
|
|
156
|
+
lastVerifiedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
await this.persistState(nextState);
|
|
159
|
+
await this.notify('마스터 코드로 라이선스가 활성화되었습니다.');
|
|
160
|
+
return nextState;
|
|
161
|
+
}
|
|
162
|
+
let verifyResult;
|
|
163
|
+
try {
|
|
164
|
+
verifyResult = await this.deps.transport.verify({
|
|
165
|
+
licenseKey: trimmedLicenseKey,
|
|
166
|
+
productId: this.config.productId,
|
|
167
|
+
instanceName: this.config.instanceName,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
throw mapLicenseError(error, '라이선스 검증 요청에 실패했습니다.');
|
|
172
|
+
}
|
|
173
|
+
if (!verifyResult.valid) {
|
|
174
|
+
throw mapLicenseError(verifyResult.raw ?? verifyResult.message, '유효하지 않은 라이선스 키입니다.');
|
|
175
|
+
}
|
|
176
|
+
if (verifyResult.productId
|
|
177
|
+
&& normalizeProductId(verifyResult.productId) !== normalizeProductId(this.config.productId)) {
|
|
178
|
+
throw new LicenseCoreError(exports.LicenseErrorCode.PRODUCT_MISMATCH, '현재 플러그인과 라이선스 상품이 일치하지 않습니다.', verifyResult);
|
|
179
|
+
}
|
|
180
|
+
const activationLimit = toNonNegativeNumber(verifyResult.activationLimit, 0);
|
|
181
|
+
const activationUsage = toNonNegativeNumber(verifyResult.activationUsage, 0);
|
|
182
|
+
if (isActivationLimitReached(activationLimit, activationUsage)) {
|
|
183
|
+
throw new LicenseCoreError(exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED, '활성화 가능한 기기 수를 모두 사용했습니다. 기존 기기를 비활성화한 뒤 다시 시도해 주세요.', verifyResult);
|
|
184
|
+
}
|
|
185
|
+
const current = await this.loadState();
|
|
186
|
+
const nextState = {
|
|
187
|
+
...current,
|
|
188
|
+
paymentStatus: 'PAID',
|
|
189
|
+
licenseKey: trimmedLicenseKey,
|
|
190
|
+
activationLimit,
|
|
191
|
+
activationUsage,
|
|
192
|
+
lastVerifiedAt: new Date().toISOString(),
|
|
193
|
+
};
|
|
194
|
+
await this.persistState(nextState);
|
|
195
|
+
await this.notify(verifyResult.message || '라이선스가 활성화되었습니다.');
|
|
196
|
+
return nextState;
|
|
197
|
+
}
|
|
198
|
+
async deactivateLicense() {
|
|
199
|
+
const current = await this.loadState();
|
|
200
|
+
if (!current.licenseKey) {
|
|
201
|
+
return current;
|
|
202
|
+
}
|
|
203
|
+
const requestPayload = {
|
|
204
|
+
licenseKey: current.licenseKey,
|
|
205
|
+
productId: this.config.productId,
|
|
206
|
+
instanceName: this.config.instanceName,
|
|
207
|
+
};
|
|
208
|
+
if (!this.config.masterCode || current.licenseKey !== this.config.masterCode) {
|
|
209
|
+
try {
|
|
210
|
+
const deactivateResult = await this.deps.transport.deactivate(requestPayload);
|
|
211
|
+
if (!deactivateResult.success) {
|
|
212
|
+
throw mapLicenseError(deactivateResult.raw ?? deactivateResult.message, '라이선스 비활성화에 실패했습니다.');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw mapLicenseError(error, '라이선스 비활성화 요청에 실패했습니다.');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const nextState = {
|
|
220
|
+
...current,
|
|
221
|
+
paymentStatus: 'FREE',
|
|
222
|
+
licenseKey: '',
|
|
223
|
+
activationLimit: 0,
|
|
224
|
+
activationUsage: 0,
|
|
225
|
+
lastVerifiedAt: undefined,
|
|
226
|
+
};
|
|
227
|
+
await this.persistState(nextState);
|
|
228
|
+
await this.notify('라이선스가 비활성화되었습니다.');
|
|
229
|
+
return nextState;
|
|
230
|
+
}
|
|
231
|
+
async incrementUsage(amount = 1) {
|
|
232
|
+
const current = await this.loadState();
|
|
233
|
+
if (current.paymentStatus === 'PAID') {
|
|
234
|
+
return current;
|
|
235
|
+
}
|
|
236
|
+
const increasedAmount = Math.max(0, amount);
|
|
237
|
+
const nextState = {
|
|
238
|
+
...current,
|
|
239
|
+
usageCount: current.usageCount + increasedAmount,
|
|
240
|
+
};
|
|
241
|
+
await this.persistState(nextState);
|
|
242
|
+
return nextState;
|
|
243
|
+
}
|
|
244
|
+
async canUseFeature() {
|
|
245
|
+
const state = await this.loadState();
|
|
246
|
+
return this.resolveUsageAccess(state);
|
|
247
|
+
}
|
|
248
|
+
resolveUsageAccess(state) {
|
|
249
|
+
if (state.paymentStatus === 'PAID') {
|
|
250
|
+
return {
|
|
251
|
+
allowed: true,
|
|
252
|
+
remaining: Number.POSITIVE_INFINITY,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const remaining = Math.max(this.usageLimit - state.usageCount, 0);
|
|
256
|
+
return {
|
|
257
|
+
allowed: remaining > 0,
|
|
258
|
+
remaining,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
getUsageLimit() {
|
|
262
|
+
return this.usageLimit;
|
|
263
|
+
}
|
|
264
|
+
getStoragePrefix() {
|
|
265
|
+
return this.storagePrefix;
|
|
266
|
+
}
|
|
267
|
+
storageKey(name) {
|
|
268
|
+
return `${this.storagePrefix}:${STORAGE_KEYS[name]}`;
|
|
269
|
+
}
|
|
270
|
+
async loadState() {
|
|
271
|
+
try {
|
|
272
|
+
const [paymentStatus, licenseKey, activationLimit, activationUsage, usageCount, lastVerifiedAt,] = await Promise.all([
|
|
273
|
+
this.deps.storage.get(this.storageKey('paymentStatus')),
|
|
274
|
+
this.deps.storage.get(this.storageKey('licenseKey')),
|
|
275
|
+
this.deps.storage.get(this.storageKey('activationLimit')),
|
|
276
|
+
this.deps.storage.get(this.storageKey('activationUsage')),
|
|
277
|
+
this.deps.storage.get(this.storageKey('usageCount')),
|
|
278
|
+
this.deps.storage.get(this.storageKey('lastVerifiedAt')),
|
|
279
|
+
]);
|
|
280
|
+
return {
|
|
281
|
+
paymentStatus: toPaymentStatus(paymentStatus),
|
|
282
|
+
licenseKey: toText(licenseKey),
|
|
283
|
+
activationLimit: toNonNegativeNumber(activationLimit, 0),
|
|
284
|
+
activationUsage: toNonNegativeNumber(activationUsage, 0),
|
|
285
|
+
usageCount: toNonNegativeNumber(usageCount, 0),
|
|
286
|
+
lastVerifiedAt: toText(lastVerifiedAt) || undefined,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
throw new LicenseCoreError(exports.LicenseErrorCode.STORAGE_ERROR, '라이선스 상태를 불러오지 못했습니다.', error, true);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async persistState(state) {
|
|
294
|
+
try {
|
|
295
|
+
await Promise.all([
|
|
296
|
+
this.deps.storage.set(this.storageKey('paymentStatus'), state.paymentStatus),
|
|
297
|
+
this.deps.storage.set(this.storageKey('licenseKey'), state.licenseKey),
|
|
298
|
+
this.deps.storage.set(this.storageKey('activationLimit'), state.activationLimit),
|
|
299
|
+
this.deps.storage.set(this.storageKey('activationUsage'), state.activationUsage),
|
|
300
|
+
this.deps.storage.set(this.storageKey('usageCount'), state.usageCount),
|
|
301
|
+
this.deps.storage.set(this.storageKey('lastVerifiedAt'), state.lastVerifiedAt || ''),
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
throw new LicenseCoreError(exports.LicenseErrorCode.STORAGE_ERROR, '라이선스 상태를 저장하지 못했습니다.', error, true);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async notify(message) {
|
|
309
|
+
if (!this.deps.notifier) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
await this.deps.notifier.notify(message);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Notification failures should not break license flow.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readTextField(raw, keys) {
|
|
322
|
+
if (!raw || typeof raw !== 'object') {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
for (const key of keys) {
|
|
326
|
+
const value = raw[key];
|
|
327
|
+
if (typeof value === 'string' && value.trim()) {
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
function readNumberField(raw, keys) {
|
|
334
|
+
if (!raw || typeof raw !== 'object') {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
for (const key of keys) {
|
|
338
|
+
const value = raw[key];
|
|
339
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
343
|
+
const numeric = Number(value);
|
|
344
|
+
if (Number.isFinite(numeric)) {
|
|
345
|
+
return numeric;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
function asRecord(value) {
|
|
352
|
+
return value && typeof value === 'object' ? value : {};
|
|
353
|
+
}
|
|
354
|
+
function defaultNormalizeVerifyResponse(raw) {
|
|
355
|
+
const body = asRecord(raw);
|
|
356
|
+
const valid = Boolean(body.valid
|
|
357
|
+
?? body.success
|
|
358
|
+
?? body.active
|
|
359
|
+
?? (typeof body.status === 'string' && body.status.toLowerCase() === 'active'));
|
|
360
|
+
const message = readTextField(body, ['message', 'error', 'detail']);
|
|
361
|
+
const nestedLicenseKey = asRecord(body.license_key);
|
|
362
|
+
const nestedMeta = asRecord(body.meta);
|
|
363
|
+
const productId = readTextField(body, ['productId', 'product_id'])
|
|
364
|
+
|| readTextField(nestedMeta, ['product_id'])
|
|
365
|
+
|| readTextField(nestedLicenseKey, ['product_id']);
|
|
366
|
+
const activationLimit = readNumberField(body, ['activationLimit', 'activation_limit'])
|
|
367
|
+
?? readNumberField(nestedMeta, ['activation_limit'])
|
|
368
|
+
?? readNumberField(nestedLicenseKey, ['activation_limit']);
|
|
369
|
+
const activationUsage = readNumberField(body, ['activationUsage', 'activation_usage'])
|
|
370
|
+
?? readNumberField(nestedMeta, ['activation_usage'])
|
|
371
|
+
?? readNumberField(nestedLicenseKey, ['activation_usage']);
|
|
372
|
+
return {
|
|
373
|
+
valid,
|
|
374
|
+
message,
|
|
375
|
+
productId,
|
|
376
|
+
activationLimit,
|
|
377
|
+
activationUsage,
|
|
378
|
+
raw,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function defaultNormalizeDeactivateResponse(raw) {
|
|
382
|
+
const body = asRecord(raw);
|
|
383
|
+
const success = Boolean(body.success
|
|
384
|
+
?? body.deactivated
|
|
385
|
+
?? body.valid
|
|
386
|
+
?? (typeof body.status === 'string' && ['ok', 'success', 'deactivated'].includes(body.status.toLowerCase())));
|
|
387
|
+
return {
|
|
388
|
+
success,
|
|
389
|
+
message: readTextField(body, ['message', 'error', 'detail']),
|
|
390
|
+
raw,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
class FetchLicenseTransport {
|
|
394
|
+
constructor(config) {
|
|
395
|
+
this.config = config;
|
|
396
|
+
if (!config.verifyUrl.trim()) {
|
|
397
|
+
throw new Error('FetchLicenseTransport.verifyUrl is required.');
|
|
398
|
+
}
|
|
399
|
+
this.fetchFn = config.fetchFn ?? fetch;
|
|
400
|
+
}
|
|
401
|
+
async verify(payload) {
|
|
402
|
+
const raw = await this.postJson(this.config.verifyUrl, this.config.buildVerifyBody ? this.config.buildVerifyBody(payload) : payload);
|
|
403
|
+
const normalize = this.config.normalizeVerifyResponse ?? defaultNormalizeVerifyResponse;
|
|
404
|
+
return normalize(raw);
|
|
405
|
+
}
|
|
406
|
+
async deactivate(payload) {
|
|
407
|
+
const url = this.config.deactivateUrl || this.config.verifyUrl;
|
|
408
|
+
const body = this.config.buildDeactivateBody
|
|
409
|
+
? this.config.buildDeactivateBody(payload)
|
|
410
|
+
: {
|
|
411
|
+
action: 'deactivate',
|
|
412
|
+
...payload,
|
|
413
|
+
};
|
|
414
|
+
const raw = await this.postJson(url, body);
|
|
415
|
+
const normalize = this.config.normalizeDeactivateResponse ?? defaultNormalizeDeactivateResponse;
|
|
416
|
+
return normalize(raw);
|
|
417
|
+
}
|
|
418
|
+
async postJson(url, body) {
|
|
419
|
+
const timeoutMs = this.config.timeoutMs ?? 15000;
|
|
420
|
+
const abortController = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
|
|
421
|
+
const timeoutId = abortController
|
|
422
|
+
? setTimeout(() => abortController.abort(), timeoutMs)
|
|
423
|
+
: undefined;
|
|
424
|
+
try {
|
|
425
|
+
const response = await this.fetchFn(url, {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: {
|
|
428
|
+
'Content-Type': 'application/json',
|
|
429
|
+
...(this.config.headers || {}),
|
|
430
|
+
},
|
|
431
|
+
body: JSON.stringify(body),
|
|
432
|
+
signal: abortController?.signal,
|
|
433
|
+
});
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
const errorText = await response.text();
|
|
436
|
+
throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`);
|
|
437
|
+
}
|
|
438
|
+
return await response.json();
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
if (timeoutId) {
|
|
442
|
+
clearTimeout(timeoutId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
class FigmaStorageAdapter {
|
|
449
|
+
constructor(figma) {
|
|
450
|
+
this.figma = figma;
|
|
451
|
+
}
|
|
452
|
+
async get(key) {
|
|
453
|
+
const value = await this.figma.clientStorage.getAsync(key);
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
async set(key, value) {
|
|
457
|
+
await this.figma.clientStorage.setAsync(key, value);
|
|
458
|
+
}
|
|
459
|
+
async delete(key) {
|
|
460
|
+
if (this.figma.clientStorage.deleteAsync) {
|
|
461
|
+
await this.figma.clientStorage.deleteAsync(key);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
await this.figma.clientStorage.setAsync(key, undefined);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
class FigmaNotifierAdapter {
|
|
468
|
+
constructor(figma) {
|
|
469
|
+
this.figma = figma;
|
|
470
|
+
}
|
|
471
|
+
notify(message, options) {
|
|
472
|
+
if (this.figma.notify) {
|
|
473
|
+
this.figma.notify(message, options);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const DEFAULT_MESSAGE_TYPES = {
|
|
478
|
+
initialize: 'initialize',
|
|
479
|
+
verifyLicense: 'verify-license',
|
|
480
|
+
deactivateLicense: 'deactivate-license',
|
|
481
|
+
updateStatus: 'update-plugin-status',
|
|
482
|
+
verifyResult: 'license-verification-result',
|
|
483
|
+
deactivateResult: 'license-deactivation-result',
|
|
484
|
+
};
|
|
485
|
+
class FigmaLicenseStatusManager {
|
|
486
|
+
constructor(options) {
|
|
487
|
+
this.options = options;
|
|
488
|
+
this.messageTypes = {
|
|
489
|
+
...DEFAULT_MESSAGE_TYPES,
|
|
490
|
+
...(options.messageTypes || {}),
|
|
491
|
+
};
|
|
492
|
+
this.notifier = options.notifier;
|
|
493
|
+
}
|
|
494
|
+
async initialize() {
|
|
495
|
+
await this.postStatusUpdate();
|
|
496
|
+
}
|
|
497
|
+
async handleMessage(pluginMessage) {
|
|
498
|
+
if (!pluginMessage || typeof pluginMessage !== 'object') {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const message = pluginMessage;
|
|
502
|
+
const type = typeof message.type === 'string' ? message.type : '';
|
|
503
|
+
if (!type) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
if (type === this.messageTypes.initialize) {
|
|
507
|
+
await this.postStatusUpdate();
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
if (type === this.messageTypes.verifyLicense) {
|
|
511
|
+
const licenseKey = typeof message.licenseKey === 'string' ? message.licenseKey : '';
|
|
512
|
+
await this.handleVerifyLicense(licenseKey);
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
if (type === this.messageTypes.deactivateLicense) {
|
|
516
|
+
await this.handleDeactivateLicense();
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
bindToOnMessage(fallbackHandler) {
|
|
522
|
+
const previousHandler = fallbackHandler ?? this.options.figma.ui.onmessage;
|
|
523
|
+
this.options.figma.ui.onmessage = async (pluginMessage) => {
|
|
524
|
+
const handled = await this.handleMessage(pluginMessage);
|
|
525
|
+
if (!handled && previousHandler) {
|
|
526
|
+
await previousHandler(pluginMessage);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
async handleVerifyLicense(licenseKey) {
|
|
531
|
+
try {
|
|
532
|
+
const state = await this.options.core.verifyLicense(licenseKey);
|
|
533
|
+
const access = this.options.core.resolveUsageAccess(state);
|
|
534
|
+
this.postMessage({
|
|
535
|
+
type: this.messageTypes.verifyResult,
|
|
536
|
+
success: true,
|
|
537
|
+
message: '라이선스가 검증되었습니다.',
|
|
538
|
+
...state,
|
|
539
|
+
usageLimit: this.options.core.getUsageLimit(),
|
|
540
|
+
canUseFeature: access.allowed,
|
|
541
|
+
remainingUsage: access.remaining,
|
|
542
|
+
});
|
|
543
|
+
await this.postStatusUpdate(state);
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
const mappedError = mapLicenseError(error, '라이선스 검증 중 오류가 발생했습니다.');
|
|
547
|
+
await this.notifyError(mappedError);
|
|
548
|
+
this.postMessage({
|
|
549
|
+
type: this.messageTypes.verifyResult,
|
|
550
|
+
success: false,
|
|
551
|
+
message: mappedError.message,
|
|
552
|
+
errorCode: mappedError.code,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async handleDeactivateLicense() {
|
|
557
|
+
try {
|
|
558
|
+
const state = await this.options.core.deactivateLicense();
|
|
559
|
+
const access = this.options.core.resolveUsageAccess(state);
|
|
560
|
+
this.postMessage({
|
|
561
|
+
type: this.messageTypes.deactivateResult,
|
|
562
|
+
success: true,
|
|
563
|
+
message: '라이선스가 비활성화되었습니다.',
|
|
564
|
+
...state,
|
|
565
|
+
usageLimit: this.options.core.getUsageLimit(),
|
|
566
|
+
canUseFeature: access.allowed,
|
|
567
|
+
remainingUsage: access.remaining,
|
|
568
|
+
});
|
|
569
|
+
await this.postStatusUpdate(state);
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
const mappedError = mapLicenseError(error, '라이선스 비활성화 중 오류가 발생했습니다.');
|
|
573
|
+
await this.notifyError(mappedError);
|
|
574
|
+
this.postMessage({
|
|
575
|
+
type: this.messageTypes.deactivateResult,
|
|
576
|
+
success: false,
|
|
577
|
+
message: mappedError.message,
|
|
578
|
+
errorCode: mappedError.code,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async postStatusUpdate(initialState) {
|
|
583
|
+
const state = initialState ?? await this.options.core.getStatus();
|
|
584
|
+
const access = this.options.core.resolveUsageAccess(state);
|
|
585
|
+
this.postMessage({
|
|
586
|
+
type: this.messageTypes.updateStatus,
|
|
587
|
+
...state,
|
|
588
|
+
usageLimit: this.options.core.getUsageLimit(),
|
|
589
|
+
canUseFeature: access.allowed,
|
|
590
|
+
remainingUsage: access.remaining,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
postMessage(message) {
|
|
594
|
+
this.options.figma.ui.postMessage(message);
|
|
595
|
+
}
|
|
596
|
+
async notifyError(error) {
|
|
597
|
+
if (!this.notifier) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
await this.notifier.notify(error.message, { error: true });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function resolveFigmaApi(figma) {
|
|
604
|
+
if (figma) {
|
|
605
|
+
return figma;
|
|
606
|
+
}
|
|
607
|
+
const globalObject = globalThis;
|
|
608
|
+
if (globalObject.figma) {
|
|
609
|
+
return globalObject.figma;
|
|
610
|
+
}
|
|
611
|
+
throw new Error('Figma API instance is required. Pass options.figma or run in Figma environment.');
|
|
612
|
+
}
|
|
613
|
+
function createFigmaLicenseStatusManager(options) {
|
|
614
|
+
const figmaApi = resolveFigmaApi(options.figma);
|
|
615
|
+
const storage = options.storage ?? new FigmaStorageAdapter(figmaApi);
|
|
616
|
+
const notifier = options.notifier ?? new FigmaNotifierAdapter(figmaApi);
|
|
617
|
+
const transport = options.transport ?? new FetchLicenseTransport({
|
|
618
|
+
verifyUrl: options.config.endpoint,
|
|
619
|
+
deactivateUrl: options.config.deactivateEndpoint,
|
|
620
|
+
headers: options.config.requestHeaders,
|
|
621
|
+
timeoutMs: options.config.requestTimeoutMs,
|
|
622
|
+
fetchFn: options.fetchFn,
|
|
623
|
+
});
|
|
624
|
+
const coreDependencies = {
|
|
625
|
+
storage,
|
|
626
|
+
transport,
|
|
627
|
+
notifier,
|
|
628
|
+
};
|
|
629
|
+
const core = new LicenseCore(options.config, coreDependencies);
|
|
630
|
+
return new FigmaLicenseStatusManager({
|
|
631
|
+
figma: figmaApi,
|
|
632
|
+
core,
|
|
633
|
+
notifier,
|
|
634
|
+
messageTypes: options.messageTypes,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
exports.FetchLicenseTransport = FetchLicenseTransport;
|
|
639
|
+
exports.FigmaLicenseStatusManager = FigmaLicenseStatusManager;
|
|
640
|
+
exports.FigmaNotifierAdapter = FigmaNotifierAdapter;
|
|
641
|
+
exports.FigmaStorageAdapter = FigmaStorageAdapter;
|
|
642
|
+
exports.LicenseCore = LicenseCore;
|
|
643
|
+
exports.LicenseCoreError = LicenseCoreError;
|
|
644
|
+
exports.createFigmaLicenseStatusManager = createFigmaLicenseStatusManager;
|
|
645
|
+
exports.defaultNormalizeDeactivateResponse = defaultNormalizeDeactivateResponse;
|
|
646
|
+
exports.defaultNormalizeVerifyResponse = defaultNormalizeVerifyResponse;
|
|
647
|
+
exports.isActivationLimitReached = isActivationLimitReached;
|
|
648
|
+
exports.mapLicenseError = mapLicenseError;
|
|
649
|
+
//# sourceMappingURL=index.js.map
|