@designbasekorea/figma-license-core 0.1.0 → 0.2.1
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 +47 -12
- package/dist/designbaseAccountAuth.d.ts +65 -0
- package/dist/designbaseAccountAuth.d.ts.map +1 -0
- package/dist/fetchLicenseTransport.d.ts +8 -0
- package/dist/fetchLicenseTransport.d.ts.map +1 -1
- package/dist/figmaStatusManager.d.ts +5 -0
- package/dist/figmaStatusManager.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +767 -52
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +769 -51
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +769 -51
- package/dist/index.umd.js.map +1 -1
- package/dist/licenseCore.d.ts +17 -4
- package/dist/licenseCore.d.ts.map +1 -1
- package/dist/licenseErrorMapper.d.ts.map +1 -1
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/PLUGIN_INTEGRATION_KO.md +315 -0
- package/package.json +3 -2
package/dist/index.umd.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
(function (LicenseErrorCode) {
|
|
9
9
|
LicenseErrorCode["ACTIVATION_LIMIT_REACHED"] = "ACTIVATION_LIMIT_REACHED";
|
|
10
10
|
LicenseErrorCode["INVALID_LICENSE"] = "INVALID_LICENSE";
|
|
11
|
+
LicenseErrorCode["LICENSE_EXPIRED"] = "LICENSE_EXPIRED";
|
|
12
|
+
LicenseErrorCode["LICENSE_REVOKED"] = "LICENSE_REVOKED";
|
|
13
|
+
LicenseErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
|
|
11
14
|
LicenseErrorCode["PRODUCT_MISMATCH"] = "PRODUCT_MISMATCH";
|
|
12
15
|
LicenseErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
13
16
|
LicenseErrorCode["STORAGE_ERROR"] = "STORAGE_ERROR";
|
|
@@ -31,10 +34,18 @@
|
|
|
31
34
|
if (typeof input === 'string') {
|
|
32
35
|
return input;
|
|
33
36
|
}
|
|
34
|
-
if (typeof input === 'object'
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
return
|
|
37
|
+
if (typeof input === 'object') {
|
|
38
|
+
const objectInput = input;
|
|
39
|
+
if (objectInput.raw) {
|
|
40
|
+
return asText(objectInput.raw);
|
|
41
|
+
}
|
|
42
|
+
if (objectInput.body) {
|
|
43
|
+
return asText(objectInput.body);
|
|
44
|
+
}
|
|
45
|
+
const fields = [objectInput.message, objectInput.errorCode, objectInput.code, objectInput.status]
|
|
46
|
+
.filter((value) => typeof value === 'string' && value.trim() !== '');
|
|
47
|
+
if (fields.length > 0) {
|
|
48
|
+
return fields.join(' ');
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
51
|
try {
|
|
@@ -52,13 +63,32 @@
|
|
|
52
63
|
|| normalized.includes('already activated on maximum')) {
|
|
53
64
|
return exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED;
|
|
54
65
|
}
|
|
66
|
+
if (normalized.includes('rate limited')
|
|
67
|
+
|| normalized.includes('rate_limited')
|
|
68
|
+
|| normalized.includes('too many requests')
|
|
69
|
+
|| normalized.includes('too_many_requests')
|
|
70
|
+
|| normalized.includes('429')) {
|
|
71
|
+
return exports.LicenseErrorCode.RATE_LIMITED;
|
|
72
|
+
}
|
|
73
|
+
if (normalized.includes('expired') || normalized.includes('expiration') || normalized.includes('license_expired')) {
|
|
74
|
+
return exports.LicenseErrorCode.LICENSE_EXPIRED;
|
|
75
|
+
}
|
|
76
|
+
if (normalized.includes('revoked')
|
|
77
|
+
|| normalized.includes('license_revoked')
|
|
78
|
+
|| normalized.includes('cancelled')
|
|
79
|
+
|| normalized.includes('canceled')) {
|
|
80
|
+
return exports.LicenseErrorCode.LICENSE_REVOKED;
|
|
81
|
+
}
|
|
55
82
|
if (normalized.includes('invalid license')
|
|
56
83
|
|| normalized.includes('not found')
|
|
57
84
|
|| normalized.includes('not valid')
|
|
58
85
|
|| normalized.includes('license key is invalid')) {
|
|
59
86
|
return exports.LicenseErrorCode.INVALID_LICENSE;
|
|
60
87
|
}
|
|
61
|
-
if (normalized.includes('product mismatch')
|
|
88
|
+
if (normalized.includes('product mismatch')
|
|
89
|
+
|| normalized.includes('product_mismatch')
|
|
90
|
+
|| normalized.includes('product_not_allowed')
|
|
91
|
+
|| normalized.includes('wrong product')) {
|
|
62
92
|
return exports.LicenseErrorCode.PRODUCT_MISMATCH;
|
|
63
93
|
}
|
|
64
94
|
if (normalized.includes('network')
|
|
@@ -79,7 +109,9 @@
|
|
|
79
109
|
const text = asText(error);
|
|
80
110
|
const message = text.trim() || fallbackMessage;
|
|
81
111
|
const code = inferCode(message);
|
|
82
|
-
return new LicenseCoreError(code, message, error, code === exports.LicenseErrorCode.NETWORK_ERROR
|
|
112
|
+
return new LicenseCoreError(code, message, error, code === exports.LicenseErrorCode.NETWORK_ERROR
|
|
113
|
+
|| code === exports.LicenseErrorCode.SERVER_ERROR
|
|
114
|
+
|| code === exports.LicenseErrorCode.RATE_LIMITED);
|
|
83
115
|
}
|
|
84
116
|
function isActivationLimitReached(activationLimit, activationUsage) {
|
|
85
117
|
if (activationLimit <= 0) {
|
|
@@ -91,10 +123,19 @@
|
|
|
91
123
|
const STORAGE_KEYS = {
|
|
92
124
|
paymentStatus: 'paymentStatus',
|
|
93
125
|
licenseKey: 'licenseKey',
|
|
126
|
+
instanceId: 'instanceId',
|
|
94
127
|
activationLimit: 'activationLimit',
|
|
95
128
|
activationUsage: 'activationUsage',
|
|
96
129
|
usageCount: 'usageCount',
|
|
130
|
+
previousUsageCount: 'previousUsageCount',
|
|
131
|
+
usageWindowKey: 'usageWindowKey',
|
|
132
|
+
planTier: 'planTier',
|
|
97
133
|
lastVerifiedAt: 'lastVerifiedAt',
|
|
134
|
+
licenseStatus: 'licenseStatus',
|
|
135
|
+
licenseSource: 'licenseSource',
|
|
136
|
+
accessScope: 'accessScope',
|
|
137
|
+
expiresAt: 'expiresAt',
|
|
138
|
+
validationMessage: 'validationMessage',
|
|
98
139
|
};
|
|
99
140
|
const DEFAULT_USAGE_LIMIT = 20;
|
|
100
141
|
const MASTER_CODE_ACTIVATION_LIMIT = 999999;
|
|
@@ -113,6 +154,31 @@
|
|
|
113
154
|
function toText(value) {
|
|
114
155
|
return typeof value === 'string' ? value : '';
|
|
115
156
|
}
|
|
157
|
+
function validationMessage(result) {
|
|
158
|
+
if (result.message) {
|
|
159
|
+
return result.message;
|
|
160
|
+
}
|
|
161
|
+
const status = (result.licenseStatus || result.errorCode || '').toLowerCase();
|
|
162
|
+
switch (status) {
|
|
163
|
+
case 'revoked':
|
|
164
|
+
case 'license_revoked':
|
|
165
|
+
return '이 멤버십 라이선스는 철회되었습니다.';
|
|
166
|
+
case 'expired':
|
|
167
|
+
case 'license_expired':
|
|
168
|
+
return '멤버십 라이선스가 만료되었습니다. YouTube 커뮤니티에서 이번 달 키를 확인해 주세요.';
|
|
169
|
+
case 'not_started':
|
|
170
|
+
return '이 멤버십 라이선스는 아직 시작되지 않았습니다.';
|
|
171
|
+
case 'rate_limited':
|
|
172
|
+
case 'rate_limited_error':
|
|
173
|
+
return '라이선스 확인 요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.';
|
|
174
|
+
case 'product_not_allowed':
|
|
175
|
+
return '이 라이선스는 현재 플러그인에서 사용할 수 없습니다.';
|
|
176
|
+
case 'network_error':
|
|
177
|
+
return '라이선스 서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.';
|
|
178
|
+
default:
|
|
179
|
+
return '라이선스가 더 이상 유효하지 않습니다.';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
116
182
|
function toPaymentStatus(value) {
|
|
117
183
|
if (value === 'PAID' || value === 'UNPAID' || value === 'FREE') {
|
|
118
184
|
return value;
|
|
@@ -122,6 +188,59 @@
|
|
|
122
188
|
function normalizeProductId(productId) {
|
|
123
189
|
return productId.trim().toLowerCase();
|
|
124
190
|
}
|
|
191
|
+
function normalizeTier(tier) {
|
|
192
|
+
return tier.trim() || 'FREE';
|
|
193
|
+
}
|
|
194
|
+
function normalizeUsageReset(reset) {
|
|
195
|
+
if (reset === 'daily' || reset === 'monthly' || reset === 'none') {
|
|
196
|
+
return reset;
|
|
197
|
+
}
|
|
198
|
+
return 'none';
|
|
199
|
+
}
|
|
200
|
+
function toDateKey(date, period) {
|
|
201
|
+
if (period === 'none') {
|
|
202
|
+
return 'lifetime';
|
|
203
|
+
}
|
|
204
|
+
const year = date.getUTCFullYear();
|
|
205
|
+
const month = `${date.getUTCMonth() + 1}`.padStart(2, '0');
|
|
206
|
+
const day = `${date.getUTCDate()}`.padStart(2, '0');
|
|
207
|
+
if (period === 'monthly') {
|
|
208
|
+
return `${year}-${month}`;
|
|
209
|
+
}
|
|
210
|
+
return `${year}-${month}-${day}`;
|
|
211
|
+
}
|
|
212
|
+
function normalizePolicy(config) {
|
|
213
|
+
const rawPolicy = config.policy || {};
|
|
214
|
+
const freeTier = normalizeTier(rawPolicy.freeTier || 'FREE');
|
|
215
|
+
const paidTier = normalizeTier(rawPolicy.paidTier || 'PAID');
|
|
216
|
+
const defaultTier = normalizeTier(rawPolicy.defaultTier || freeTier);
|
|
217
|
+
const usageByTier = {
|
|
218
|
+
[freeTier]: {
|
|
219
|
+
limit: Math.max(config.usageLimit ?? DEFAULT_USAGE_LIMIT, 0),
|
|
220
|
+
reset: 'none',
|
|
221
|
+
},
|
|
222
|
+
[paidTier]: {
|
|
223
|
+
limit: 'unlimited',
|
|
224
|
+
reset: 'none',
|
|
225
|
+
},
|
|
226
|
+
...(rawPolicy.usageByTier || {}),
|
|
227
|
+
};
|
|
228
|
+
Object.keys(usageByTier).forEach((tier) => {
|
|
229
|
+
usageByTier[tier] = {
|
|
230
|
+
limit: usageByTier[tier].limit === 'unlimited'
|
|
231
|
+
? 'unlimited'
|
|
232
|
+
: Math.max(toNonNegativeNumber(usageByTier[tier].limit, 0), 0),
|
|
233
|
+
reset: normalizeUsageReset(usageByTier[tier].reset),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
defaultTier,
|
|
238
|
+
freeTier,
|
|
239
|
+
paidTier,
|
|
240
|
+
usageByTier,
|
|
241
|
+
featuresByTier: rawPolicy.featuresByTier || {},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
125
244
|
class LicenseCore {
|
|
126
245
|
constructor(config, deps) {
|
|
127
246
|
this.config = config;
|
|
@@ -135,14 +254,117 @@
|
|
|
135
254
|
if (!config.instanceName.trim()) {
|
|
136
255
|
throw new Error('LicenseConfig.instanceName is required.');
|
|
137
256
|
}
|
|
138
|
-
this.
|
|
257
|
+
this.policy = normalizePolicy(config);
|
|
139
258
|
this.storagePrefix = config.storagePrefix?.trim() || `${config.instanceName}:${config.productId}`;
|
|
140
259
|
}
|
|
141
260
|
async initialize() {
|
|
142
|
-
|
|
261
|
+
let state = await this.getStatus();
|
|
262
|
+
if (state.paymentStatus === 'PAID' && state.licenseKey && state.licenseKey !== this.config.masterCode) {
|
|
263
|
+
try {
|
|
264
|
+
const validateResult = this.deps.transport.validate
|
|
265
|
+
? await this.deps.transport.validate({
|
|
266
|
+
licenseKey: state.licenseKey,
|
|
267
|
+
productId: this.config.productId,
|
|
268
|
+
instanceName: this.config.instanceName,
|
|
269
|
+
instanceId: state.instanceId,
|
|
270
|
+
})
|
|
271
|
+
: await this.deps.transport.verify({
|
|
272
|
+
licenseKey: state.licenseKey,
|
|
273
|
+
productId: this.config.productId,
|
|
274
|
+
instanceName: this.config.instanceName,
|
|
275
|
+
});
|
|
276
|
+
if (!validateResult.valid) {
|
|
277
|
+
state = await this.markValidationFailure(state, validationMessage(validateResult), validateResult.errorCode || validateResult.licenseStatus);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
state = await this.applyValidationResult(state, validateResult);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
const mappedError = mapLicenseError(error, '라이선스 검증 요청에 실패했습니다.');
|
|
285
|
+
state = await this.markValidationFailure(state, mappedError.message, mappedError.code);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return this.applyUsageWindowResetIfNeeded(state);
|
|
289
|
+
}
|
|
290
|
+
async resetLicenseState() {
|
|
291
|
+
const current = await this.getStatus();
|
|
292
|
+
const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
|
|
293
|
+
const previousUsageCount = current.previousUsageCount ?? 0;
|
|
294
|
+
const nextState = {
|
|
295
|
+
...current,
|
|
296
|
+
paymentStatus: 'FREE',
|
|
297
|
+
planTier: this.policy.freeTier,
|
|
298
|
+
usageWindowKey: toDateKey(new Date(), freeReset),
|
|
299
|
+
licenseKey: '',
|
|
300
|
+
instanceId: '',
|
|
301
|
+
activationLimit: 0,
|
|
302
|
+
activationUsage: 0,
|
|
303
|
+
usageCount: previousUsageCount,
|
|
304
|
+
lastVerifiedAt: undefined,
|
|
305
|
+
licenseStatus: undefined,
|
|
306
|
+
licenseSource: undefined,
|
|
307
|
+
accessScope: undefined,
|
|
308
|
+
expiresAt: undefined,
|
|
309
|
+
validationMessage: undefined,
|
|
310
|
+
};
|
|
311
|
+
await this.persistState(nextState);
|
|
312
|
+
return nextState;
|
|
313
|
+
}
|
|
314
|
+
async applyValidationResult(current, result) {
|
|
315
|
+
const nextState = {
|
|
316
|
+
...current,
|
|
317
|
+
paymentStatus: 'PAID',
|
|
318
|
+
activationLimit: result.activationLimit ?? current.activationLimit,
|
|
319
|
+
activationUsage: result.activationUsage ?? current.activationUsage,
|
|
320
|
+
licenseStatus: result.licenseStatus || 'active',
|
|
321
|
+
licenseSource: result.licenseSource || current.licenseSource,
|
|
322
|
+
accessScope: result.accessScope || current.accessScope,
|
|
323
|
+
expiresAt: result.expiresAt ?? current.expiresAt,
|
|
324
|
+
validationMessage: result.message || undefined,
|
|
325
|
+
lastVerifiedAt: new Date().toISOString(),
|
|
326
|
+
};
|
|
327
|
+
await this.persistState(nextState);
|
|
328
|
+
return nextState;
|
|
329
|
+
}
|
|
330
|
+
async markValidationFailure(current, message, errorCode) {
|
|
331
|
+
const freeReset = this.getUsagePolicy(this.policy.freeTier).reset;
|
|
332
|
+
const nextState = {
|
|
333
|
+
...current,
|
|
334
|
+
paymentStatus: 'UNPAID',
|
|
335
|
+
planTier: this.policy.freeTier,
|
|
336
|
+
usageWindowKey: toDateKey(new Date(), freeReset),
|
|
337
|
+
activationLimit: 0,
|
|
338
|
+
activationUsage: 0,
|
|
339
|
+
usageCount: current.previousUsageCount ?? current.usageCount,
|
|
340
|
+
licenseStatus: errorCode || 'invalid',
|
|
341
|
+
accessScope: undefined,
|
|
342
|
+
expiresAt: undefined,
|
|
343
|
+
validationMessage: message,
|
|
344
|
+
lastVerifiedAt: new Date().toISOString(),
|
|
345
|
+
};
|
|
346
|
+
await this.persistState(nextState);
|
|
347
|
+
return nextState;
|
|
143
348
|
}
|
|
144
349
|
async getStatus() {
|
|
145
|
-
|
|
350
|
+
const state = await this.loadState();
|
|
351
|
+
return this.applyUsageWindowResetIfNeeded(state);
|
|
352
|
+
}
|
|
353
|
+
async getLicenseSummary() {
|
|
354
|
+
const state = await this.getStatus();
|
|
355
|
+
return {
|
|
356
|
+
paymentStatus: state.paymentStatus,
|
|
357
|
+
planTier: this.resolveTier(state),
|
|
358
|
+
licenseKey: state.licenseKey,
|
|
359
|
+
hasLicenseKey: Boolean(state.licenseKey),
|
|
360
|
+
isLicenseRegistered: this.isLicenseRegisteredState(state),
|
|
361
|
+
licenseStatus: state.licenseStatus,
|
|
362
|
+
licenseSource: state.licenseSource,
|
|
363
|
+
accessScope: state.accessScope,
|
|
364
|
+
expiresAt: state.expiresAt,
|
|
365
|
+
lastVerifiedAt: state.lastVerifiedAt,
|
|
366
|
+
validationMessage: state.validationMessage,
|
|
367
|
+
};
|
|
146
368
|
}
|
|
147
369
|
async verifyLicense(licenseKey) {
|
|
148
370
|
const trimmedLicenseKey = licenseKey.trim();
|
|
@@ -150,18 +372,27 @@
|
|
|
150
372
|
throw new LicenseCoreError(exports.LicenseErrorCode.INVALID_LICENSE, '라이선스 키를 입력해 주세요.');
|
|
151
373
|
}
|
|
152
374
|
if (this.config.masterCode && trimmedLicenseKey === this.config.masterCode) {
|
|
153
|
-
const current = await this.
|
|
375
|
+
const current = await this.getStatus();
|
|
154
376
|
const nextState = {
|
|
155
377
|
...current,
|
|
156
378
|
paymentStatus: 'PAID',
|
|
379
|
+
planTier: this.policy.paidTier,
|
|
380
|
+
usageWindowKey: toDateKey(new Date(), this.getUsagePolicy(this.policy.paidTier).reset),
|
|
157
381
|
licenseKey: trimmedLicenseKey,
|
|
382
|
+
instanceId: 'master-instance',
|
|
158
383
|
activationLimit: MASTER_CODE_ACTIVATION_LIMIT,
|
|
159
384
|
activationUsage: 0,
|
|
385
|
+
previousUsageCount: current.usageCount,
|
|
160
386
|
lastVerifiedAt: new Date().toISOString(),
|
|
387
|
+
licenseStatus: 'active',
|
|
388
|
+
licenseSource: 'master',
|
|
389
|
+
accessScope: 'all-products',
|
|
390
|
+
expiresAt: undefined,
|
|
391
|
+
validationMessage: undefined,
|
|
161
392
|
};
|
|
162
393
|
await this.persistState(nextState);
|
|
163
394
|
await this.notify('마스터 코드로 라이선스가 활성화되었습니다.');
|
|
164
|
-
return nextState;
|
|
395
|
+
return this.applyUsageWindowResetIfNeeded(nextState);
|
|
165
396
|
}
|
|
166
397
|
let verifyResult;
|
|
167
398
|
try {
|
|
@@ -175,7 +406,7 @@
|
|
|
175
406
|
throw mapLicenseError(error, '라이선스 검증 요청에 실패했습니다.');
|
|
176
407
|
}
|
|
177
408
|
if (!verifyResult.valid) {
|
|
178
|
-
throw mapLicenseError(verifyResult.raw ?? verifyResult.message, '유효하지 않은 라이선스 키입니다.');
|
|
409
|
+
throw mapLicenseError(verifyResult.raw ?? { message: verifyResult.message, errorCode: verifyResult.errorCode }, '유효하지 않은 라이선스 키입니다.');
|
|
179
410
|
}
|
|
180
411
|
if (verifyResult.productId
|
|
181
412
|
&& normalizeProductId(verifyResult.productId) !== normalizeProductId(this.config.productId)) {
|
|
@@ -186,21 +417,32 @@
|
|
|
186
417
|
if (isActivationLimitReached(activationLimit, activationUsage)) {
|
|
187
418
|
throw new LicenseCoreError(exports.LicenseErrorCode.ACTIVATION_LIMIT_REACHED, '활성화 가능한 기기 수를 모두 사용했습니다. 기존 기기를 비활성화한 뒤 다시 시도해 주세요.', verifyResult);
|
|
188
419
|
}
|
|
189
|
-
const current = await this.
|
|
420
|
+
const current = await this.getStatus();
|
|
421
|
+
const planTier = normalizeTier(verifyResult.planTier || this.policy.paidTier);
|
|
422
|
+
const usageReset = this.getUsagePolicy(planTier).reset;
|
|
190
423
|
const nextState = {
|
|
191
424
|
...current,
|
|
192
425
|
paymentStatus: 'PAID',
|
|
193
|
-
|
|
426
|
+
planTier,
|
|
427
|
+
usageWindowKey: toDateKey(new Date(), usageReset),
|
|
428
|
+
licenseKey: verifyResult.licenseKey || trimmedLicenseKey,
|
|
429
|
+
instanceId: verifyResult.instanceId || '',
|
|
194
430
|
activationLimit,
|
|
195
431
|
activationUsage,
|
|
432
|
+
previousUsageCount: current.usageCount,
|
|
196
433
|
lastVerifiedAt: new Date().toISOString(),
|
|
434
|
+
licenseStatus: verifyResult.licenseStatus || 'active',
|
|
435
|
+
licenseSource: verifyResult.licenseSource,
|
|
436
|
+
accessScope: verifyResult.accessScope,
|
|
437
|
+
expiresAt: verifyResult.expiresAt,
|
|
438
|
+
validationMessage: verifyResult.message || undefined,
|
|
197
439
|
};
|
|
198
440
|
await this.persistState(nextState);
|
|
199
441
|
await this.notify(verifyResult.message || '라이선스가 활성화되었습니다.');
|
|
200
|
-
return nextState;
|
|
442
|
+
return this.applyUsageWindowResetIfNeeded(nextState);
|
|
201
443
|
}
|
|
202
444
|
async deactivateLicense() {
|
|
203
|
-
const current = await this.
|
|
445
|
+
const current = await this.getStatus();
|
|
204
446
|
if (!current.licenseKey) {
|
|
205
447
|
return current;
|
|
206
448
|
}
|
|
@@ -208,6 +450,7 @@
|
|
|
208
450
|
licenseKey: current.licenseKey,
|
|
209
451
|
productId: this.config.productId,
|
|
210
452
|
instanceName: this.config.instanceName,
|
|
453
|
+
instanceId: current.instanceId,
|
|
211
454
|
};
|
|
212
455
|
if (!this.config.masterCode || current.licenseKey !== this.config.masterCode) {
|
|
213
456
|
try {
|
|
@@ -220,24 +463,19 @@
|
|
|
220
463
|
throw mapLicenseError(error, '라이선스 비활성화 요청에 실패했습니다.');
|
|
221
464
|
}
|
|
222
465
|
}
|
|
223
|
-
const nextState =
|
|
224
|
-
...current,
|
|
225
|
-
paymentStatus: 'FREE',
|
|
226
|
-
licenseKey: '',
|
|
227
|
-
activationLimit: 0,
|
|
228
|
-
activationUsage: 0,
|
|
229
|
-
lastVerifiedAt: undefined,
|
|
230
|
-
};
|
|
231
|
-
await this.persistState(nextState);
|
|
466
|
+
const nextState = await this.resetLicenseState();
|
|
232
467
|
await this.notify('라이선스가 비활성화되었습니다.');
|
|
233
|
-
return nextState;
|
|
468
|
+
return this.applyUsageWindowResetIfNeeded(nextState);
|
|
234
469
|
}
|
|
235
470
|
async incrementUsage(amount = 1) {
|
|
236
|
-
const current = await this.
|
|
471
|
+
const current = await this.getStatus();
|
|
237
472
|
if (current.paymentStatus === 'PAID') {
|
|
238
473
|
return current;
|
|
239
474
|
}
|
|
240
475
|
const increasedAmount = Math.max(0, amount);
|
|
476
|
+
if (increasedAmount === 0) {
|
|
477
|
+
return current;
|
|
478
|
+
}
|
|
241
479
|
const nextState = {
|
|
242
480
|
...current,
|
|
243
481
|
usageCount: current.usageCount + increasedAmount,
|
|
@@ -245,64 +483,186 @@
|
|
|
245
483
|
await this.persistState(nextState);
|
|
246
484
|
return nextState;
|
|
247
485
|
}
|
|
248
|
-
async canUseFeature() {
|
|
249
|
-
const state = await this.
|
|
250
|
-
return this.resolveUsageAccess(state);
|
|
486
|
+
async canUseFeature(featureKey) {
|
|
487
|
+
const state = await this.getStatus();
|
|
488
|
+
return this.resolveUsageAccess(state, featureKey);
|
|
251
489
|
}
|
|
252
|
-
|
|
253
|
-
|
|
490
|
+
async consumeUsage(amount = 1, featureKey) {
|
|
491
|
+
const state = await this.getStatus();
|
|
492
|
+
const access = this.resolveUsageAccess(state, featureKey);
|
|
493
|
+
if (!access.allowed || amount <= 0) {
|
|
494
|
+
return { state, access };
|
|
495
|
+
}
|
|
496
|
+
const updatedState = await this.incrementUsage(amount);
|
|
497
|
+
const updatedAccess = this.resolveUsageAccess(updatedState, featureKey);
|
|
498
|
+
return {
|
|
499
|
+
state: updatedState,
|
|
500
|
+
access: updatedAccess,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
resolveUsageAccess(state, featureKey) {
|
|
504
|
+
const tier = this.resolveTier(state);
|
|
505
|
+
const usagePolicy = this.getUsagePolicy(tier);
|
|
506
|
+
const featureAllowed = this.isFeatureAllowed(tier, featureKey);
|
|
507
|
+
const isLicensed = this.isLicenseRegisteredState(state);
|
|
508
|
+
const used = state.usageCount;
|
|
509
|
+
if (usagePolicy.limit === 'unlimited') {
|
|
254
510
|
return {
|
|
255
|
-
allowed:
|
|
511
|
+
allowed: featureAllowed,
|
|
256
512
|
remaining: Number.POSITIVE_INFINITY,
|
|
513
|
+
used,
|
|
514
|
+
limit: 'unlimited',
|
|
515
|
+
reset: usagePolicy.reset,
|
|
516
|
+
tier,
|
|
517
|
+
hasLicenseKey: Boolean(state.licenseKey),
|
|
518
|
+
isLicenseRegistered: isLicensed,
|
|
519
|
+
featureAllowed,
|
|
520
|
+
featureKey,
|
|
521
|
+
reason: featureAllowed ? undefined : 'FEATURE_NOT_ALLOWED',
|
|
257
522
|
};
|
|
258
523
|
}
|
|
259
|
-
const remaining = Math.max(
|
|
524
|
+
const remaining = Math.max(usagePolicy.limit - used, 0);
|
|
525
|
+
const usageAllowed = remaining > 0;
|
|
526
|
+
const allowed = featureAllowed && usageAllowed;
|
|
260
527
|
return {
|
|
261
|
-
allowed
|
|
528
|
+
allowed,
|
|
262
529
|
remaining,
|
|
530
|
+
used,
|
|
531
|
+
limit: usagePolicy.limit,
|
|
532
|
+
reset: usagePolicy.reset,
|
|
533
|
+
tier,
|
|
534
|
+
hasLicenseKey: Boolean(state.licenseKey),
|
|
535
|
+
isLicenseRegistered: isLicensed,
|
|
536
|
+
featureAllowed,
|
|
537
|
+
featureKey,
|
|
538
|
+
reason: featureAllowed ? (usageAllowed ? undefined : 'USAGE_LIMIT_REACHED') : 'FEATURE_NOT_ALLOWED',
|
|
263
539
|
};
|
|
264
540
|
}
|
|
265
541
|
getUsageLimit() {
|
|
266
|
-
|
|
542
|
+
const freePolicy = this.getUsagePolicy(this.policy.freeTier);
|
|
543
|
+
return freePolicy.limit === 'unlimited' ? Number.POSITIVE_INFINITY : freePolicy.limit;
|
|
267
544
|
}
|
|
268
545
|
getStoragePrefix() {
|
|
269
546
|
return this.storagePrefix;
|
|
270
547
|
}
|
|
548
|
+
resolveTier(state) {
|
|
549
|
+
const storedTier = normalizeTier(state.planTier || '');
|
|
550
|
+
if (storedTier) {
|
|
551
|
+
return storedTier;
|
|
552
|
+
}
|
|
553
|
+
return state.paymentStatus === 'PAID' ? this.policy.paidTier : this.policy.freeTier;
|
|
554
|
+
}
|
|
555
|
+
getUsagePolicy(tier) {
|
|
556
|
+
return this.policy.usageByTier[tier]
|
|
557
|
+
|| this.policy.usageByTier[this.policy.defaultTier]
|
|
558
|
+
|| { limit: 'unlimited', reset: 'none' };
|
|
559
|
+
}
|
|
560
|
+
isFeatureAllowed(tier, featureKey) {
|
|
561
|
+
if (!featureKey) {
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
const policy = this.policy.featuresByTier[tier];
|
|
565
|
+
if (!policy) {
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
if (policy.deny?.includes(featureKey)) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
if (policy.allow && policy.allow.length > 0) {
|
|
572
|
+
return policy.allow.includes(featureKey);
|
|
573
|
+
}
|
|
574
|
+
return policy.defaultAllowed ?? true;
|
|
575
|
+
}
|
|
576
|
+
isLicenseRegisteredState(state) {
|
|
577
|
+
const status = state.licenseStatus?.toLowerCase();
|
|
578
|
+
const invalidStatuses = ['invalid', 'inactive', 'expired', 'revoked', 'unpaid'];
|
|
579
|
+
return state.paymentStatus === 'PAID'
|
|
580
|
+
&& Boolean(state.licenseKey)
|
|
581
|
+
&& !invalidStatuses.includes(status || '');
|
|
582
|
+
}
|
|
271
583
|
storageKey(name) {
|
|
272
584
|
return `${this.storagePrefix}:${STORAGE_KEYS[name]}`;
|
|
273
585
|
}
|
|
274
586
|
async loadState() {
|
|
275
587
|
try {
|
|
276
|
-
const [paymentStatus, licenseKey, activationLimit, activationUsage, usageCount, lastVerifiedAt,] = await Promise.all([
|
|
588
|
+
const [paymentStatus, licenseKey, instanceId, activationLimit, activationUsage, usageCount, previousUsageCount, usageWindowKey, planTier, lastVerifiedAt, licenseStatus, licenseSource, accessScope, expiresAt, validationMessage,] = await Promise.all([
|
|
277
589
|
this.deps.storage.get(this.storageKey('paymentStatus')),
|
|
278
590
|
this.deps.storage.get(this.storageKey('licenseKey')),
|
|
591
|
+
this.deps.storage.get(this.storageKey('instanceId')),
|
|
279
592
|
this.deps.storage.get(this.storageKey('activationLimit')),
|
|
280
593
|
this.deps.storage.get(this.storageKey('activationUsage')),
|
|
281
594
|
this.deps.storage.get(this.storageKey('usageCount')),
|
|
595
|
+
this.deps.storage.get(this.storageKey('previousUsageCount')),
|
|
596
|
+
this.deps.storage.get(this.storageKey('usageWindowKey')),
|
|
597
|
+
this.deps.storage.get(this.storageKey('planTier')),
|
|
282
598
|
this.deps.storage.get(this.storageKey('lastVerifiedAt')),
|
|
599
|
+
this.deps.storage.get(this.storageKey('licenseStatus')),
|
|
600
|
+
this.deps.storage.get(this.storageKey('licenseSource')),
|
|
601
|
+
this.deps.storage.get(this.storageKey('accessScope')),
|
|
602
|
+
this.deps.storage.get(this.storageKey('expiresAt')),
|
|
603
|
+
this.deps.storage.get(this.storageKey('validationMessage')),
|
|
283
604
|
]);
|
|
605
|
+
const normalizedPaymentStatus = toPaymentStatus(paymentStatus);
|
|
606
|
+
const resolvedTier = normalizeTier(toText(planTier)
|
|
607
|
+
|| (normalizedPaymentStatus === 'PAID' ? this.policy.paidTier : this.policy.freeTier));
|
|
608
|
+
const usageReset = this.getUsagePolicy(resolvedTier).reset;
|
|
284
609
|
return {
|
|
285
|
-
paymentStatus:
|
|
610
|
+
paymentStatus: normalizedPaymentStatus,
|
|
286
611
|
licenseKey: toText(licenseKey),
|
|
612
|
+
instanceId: toText(instanceId),
|
|
287
613
|
activationLimit: toNonNegativeNumber(activationLimit, 0),
|
|
288
614
|
activationUsage: toNonNegativeNumber(activationUsage, 0),
|
|
289
615
|
usageCount: toNonNegativeNumber(usageCount, 0),
|
|
616
|
+
previousUsageCount: toNonNegativeNumber(previousUsageCount, 0),
|
|
617
|
+
usageWindowKey: toText(usageWindowKey) || toDateKey(new Date(), usageReset),
|
|
618
|
+
planTier: resolvedTier,
|
|
290
619
|
lastVerifiedAt: toText(lastVerifiedAt) || undefined,
|
|
620
|
+
licenseStatus: toText(licenseStatus) || undefined,
|
|
621
|
+
licenseSource: toText(licenseSource) || undefined,
|
|
622
|
+
accessScope: toText(accessScope) || undefined,
|
|
623
|
+
expiresAt: toText(expiresAt) || undefined,
|
|
624
|
+
validationMessage: toText(validationMessage) || undefined,
|
|
291
625
|
};
|
|
292
626
|
}
|
|
293
627
|
catch (error) {
|
|
294
628
|
throw new LicenseCoreError(exports.LicenseErrorCode.STORAGE_ERROR, '라이선스 상태를 불러오지 못했습니다.', error, true);
|
|
295
629
|
}
|
|
296
630
|
}
|
|
631
|
+
async applyUsageWindowResetIfNeeded(state) {
|
|
632
|
+
const tier = this.resolveTier(state);
|
|
633
|
+
const usageReset = this.getUsagePolicy(tier).reset;
|
|
634
|
+
const expectedWindowKey = toDateKey(new Date(), usageReset);
|
|
635
|
+
const requiresUsageReset = usageReset !== 'none' && state.usageWindowKey !== expectedWindowKey;
|
|
636
|
+
const requiresWindowMigration = state.usageWindowKey !== expectedWindowKey;
|
|
637
|
+
if (!requiresUsageReset && !requiresWindowMigration) {
|
|
638
|
+
return state;
|
|
639
|
+
}
|
|
640
|
+
const nextState = {
|
|
641
|
+
...state,
|
|
642
|
+
usageWindowKey: expectedWindowKey,
|
|
643
|
+
usageCount: requiresUsageReset ? 0 : state.usageCount,
|
|
644
|
+
};
|
|
645
|
+
await this.persistState(nextState);
|
|
646
|
+
return nextState;
|
|
647
|
+
}
|
|
297
648
|
async persistState(state) {
|
|
298
649
|
try {
|
|
299
650
|
await Promise.all([
|
|
300
651
|
this.deps.storage.set(this.storageKey('paymentStatus'), state.paymentStatus),
|
|
301
652
|
this.deps.storage.set(this.storageKey('licenseKey'), state.licenseKey),
|
|
653
|
+
this.deps.storage.set(this.storageKey('instanceId'), state.instanceId || ''),
|
|
302
654
|
this.deps.storage.set(this.storageKey('activationLimit'), state.activationLimit),
|
|
303
655
|
this.deps.storage.set(this.storageKey('activationUsage'), state.activationUsage),
|
|
304
656
|
this.deps.storage.set(this.storageKey('usageCount'), state.usageCount),
|
|
657
|
+
this.deps.storage.set(this.storageKey('previousUsageCount'), state.previousUsageCount || 0),
|
|
658
|
+
this.deps.storage.set(this.storageKey('usageWindowKey'), state.usageWindowKey),
|
|
659
|
+
this.deps.storage.set(this.storageKey('planTier'), state.planTier),
|
|
305
660
|
this.deps.storage.set(this.storageKey('lastVerifiedAt'), state.lastVerifiedAt || ''),
|
|
661
|
+
this.deps.storage.set(this.storageKey('licenseStatus'), state.licenseStatus || ''),
|
|
662
|
+
this.deps.storage.set(this.storageKey('licenseSource'), state.licenseSource || ''),
|
|
663
|
+
this.deps.storage.set(this.storageKey('accessScope'), state.accessScope || ''),
|
|
664
|
+
this.deps.storage.set(this.storageKey('expiresAt'), state.expiresAt || ''),
|
|
665
|
+
this.deps.storage.set(this.storageKey('validationMessage'), state.validationMessage || ''),
|
|
306
666
|
]);
|
|
307
667
|
}
|
|
308
668
|
catch (error) {
|
|
@@ -352,18 +712,47 @@
|
|
|
352
712
|
}
|
|
353
713
|
return undefined;
|
|
354
714
|
}
|
|
355
|
-
function
|
|
715
|
+
function readBooleanField(raw, keys) {
|
|
716
|
+
if (!raw || typeof raw !== 'object') {
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
for (const key of keys) {
|
|
720
|
+
const value = raw[key];
|
|
721
|
+
if (typeof value === 'boolean') {
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
if (typeof value === 'string') {
|
|
725
|
+
const normalized = value.trim().toLowerCase();
|
|
726
|
+
if (['true', '1', 'yes', 'active', 'valid', 'success'].includes(normalized)) {
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
if (['false', '0', 'no', 'inactive', 'invalid', 'revoked', 'expired', 'error'].includes(normalized)) {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
function asRecord$1(value) {
|
|
356
737
|
return value && typeof value === 'object' ? value : {};
|
|
357
738
|
}
|
|
358
739
|
function defaultNormalizeVerifyResponse(raw) {
|
|
359
|
-
const body = asRecord(raw);
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
740
|
+
const body = asRecord$1(raw);
|
|
741
|
+
const licenseStatus = readTextField(body, ['licenseStatus', 'license_status', 'status'])
|
|
742
|
+
|| readTextField(asRecord$1(body.meta), ['license_status', 'status']);
|
|
743
|
+
const statusValid = typeof licenseStatus === 'string'
|
|
744
|
+
? licenseStatus.toLowerCase() === 'active'
|
|
745
|
+
: undefined;
|
|
746
|
+
const valid = readBooleanField(body, ['valid', 'success', 'active'])
|
|
747
|
+
?? statusValid
|
|
748
|
+
?? false;
|
|
364
749
|
const message = readTextField(body, ['message', 'error', 'detail']);
|
|
365
|
-
const nestedLicenseKey = asRecord(body.license_key);
|
|
366
|
-
const nestedMeta = asRecord(body.meta);
|
|
750
|
+
const nestedLicenseKey = asRecord$1(body.license_key);
|
|
751
|
+
const nestedMeta = asRecord$1(body.meta);
|
|
752
|
+
const nestedInstance = asRecord$1(body.instance);
|
|
753
|
+
const instanceId = readTextField(body, ['instanceId', 'instance_id'])
|
|
754
|
+
|| readTextField(nestedInstance, ['id'])
|
|
755
|
+
|| readTextField(nestedLicenseKey, ['instance_id']);
|
|
367
756
|
const productId = readTextField(body, ['productId', 'product_id'])
|
|
368
757
|
|| readTextField(nestedMeta, ['product_id'])
|
|
369
758
|
|| readTextField(nestedLicenseKey, ['product_id']);
|
|
@@ -373,17 +762,37 @@
|
|
|
373
762
|
const activationUsage = readNumberField(body, ['activationUsage', 'activation_usage'])
|
|
374
763
|
?? readNumberField(nestedMeta, ['activation_usage'])
|
|
375
764
|
?? readNumberField(nestedLicenseKey, ['activation_usage']);
|
|
765
|
+
const planTier = readTextField(body, ['planTier', 'tier', 'plan'])
|
|
766
|
+
|| readTextField(nestedMeta, ['tier', 'plan']);
|
|
767
|
+
const licenseSource = readTextField(body, ['licenseSource', 'license_source', 'source'])
|
|
768
|
+
|| readTextField(nestedMeta, ['license_source', 'source'])
|
|
769
|
+
|| readTextField(nestedLicenseKey, ['license_source', 'source']);
|
|
770
|
+
const accessScope = readTextField(body, ['accessScope', 'access_scope', 'scope'])
|
|
771
|
+
|| readTextField(nestedMeta, ['access_scope', 'scope'])
|
|
772
|
+
|| readTextField(nestedLicenseKey, ['access_scope', 'scope']);
|
|
773
|
+
const expiresAt = readTextField(body, ['expiresAt', 'expires_at', 'expiration', 'expiration_date'])
|
|
774
|
+
|| readTextField(nestedMeta, ['expires_at', 'expiration', 'expiration_date'])
|
|
775
|
+
|| readTextField(nestedLicenseKey, ['expires_at', 'expiration', 'expiration_date']);
|
|
776
|
+
const errorCode = readTextField(body, ['errorCode', 'error_code', 'code'])
|
|
777
|
+
|| readTextField(nestedMeta, ['error_code', 'code']);
|
|
376
778
|
return {
|
|
377
779
|
valid,
|
|
378
780
|
message,
|
|
379
781
|
productId,
|
|
380
782
|
activationLimit,
|
|
381
783
|
activationUsage,
|
|
784
|
+
instanceId,
|
|
785
|
+
planTier,
|
|
786
|
+
licenseStatus,
|
|
787
|
+
licenseSource,
|
|
788
|
+
accessScope,
|
|
789
|
+
expiresAt,
|
|
790
|
+
errorCode,
|
|
382
791
|
raw,
|
|
383
792
|
};
|
|
384
793
|
}
|
|
385
794
|
function defaultNormalizeDeactivateResponse(raw) {
|
|
386
|
-
const body = asRecord(raw);
|
|
795
|
+
const body = asRecord$1(raw);
|
|
387
796
|
const success = Boolean(body.success
|
|
388
797
|
?? body.deactivated
|
|
389
798
|
?? body.valid
|
|
@@ -394,6 +803,14 @@
|
|
|
394
803
|
raw,
|
|
395
804
|
};
|
|
396
805
|
}
|
|
806
|
+
class LicenseTransportHttpError extends Error {
|
|
807
|
+
constructor(status, body, message) {
|
|
808
|
+
super(message);
|
|
809
|
+
this.status = status;
|
|
810
|
+
this.body = body;
|
|
811
|
+
this.name = 'LicenseTransportHttpError';
|
|
812
|
+
}
|
|
813
|
+
}
|
|
397
814
|
class FetchLicenseTransport {
|
|
398
815
|
constructor(config) {
|
|
399
816
|
this.config = config;
|
|
@@ -407,6 +824,15 @@
|
|
|
407
824
|
const normalize = this.config.normalizeVerifyResponse ?? defaultNormalizeVerifyResponse;
|
|
408
825
|
return normalize(raw);
|
|
409
826
|
}
|
|
827
|
+
async validate(payload) {
|
|
828
|
+
const url = this.config.validateUrl || this.config.verifyUrl;
|
|
829
|
+
const body = this.config.buildValidateBody
|
|
830
|
+
? this.config.buildValidateBody(payload)
|
|
831
|
+
: payload;
|
|
832
|
+
const raw = await this.postJson(url, body);
|
|
833
|
+
const normalize = this.config.normalizeVerifyResponse ?? defaultNormalizeVerifyResponse;
|
|
834
|
+
return normalize(raw);
|
|
835
|
+
}
|
|
410
836
|
async deactivate(payload) {
|
|
411
837
|
const url = this.config.deactivateUrl || this.config.verifyUrl;
|
|
412
838
|
const body = this.config.buildDeactivateBody
|
|
@@ -436,8 +862,143 @@
|
|
|
436
862
|
signal: abortController?.signal,
|
|
437
863
|
});
|
|
438
864
|
if (!response.ok) {
|
|
439
|
-
|
|
440
|
-
|
|
865
|
+
let errorBody;
|
|
866
|
+
try {
|
|
867
|
+
errorBody = await response.json();
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
errorBody = await response.text();
|
|
871
|
+
}
|
|
872
|
+
const errorRecord = asRecord$1(errorBody);
|
|
873
|
+
const errorMessage = readTextField(errorRecord, ['message', 'error', 'detail'])
|
|
874
|
+
|| (typeof errorBody === 'string' ? errorBody : '')
|
|
875
|
+
|| response.statusText;
|
|
876
|
+
throw new LicenseTransportHttpError(response.status, errorBody, `HTTP ${response.status}: ${errorMessage}`);
|
|
877
|
+
}
|
|
878
|
+
return await response.json();
|
|
879
|
+
}
|
|
880
|
+
finally {
|
|
881
|
+
if (timeoutId) {
|
|
882
|
+
clearTimeout(timeoutId);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function asRecord(value) {
|
|
889
|
+
return value && typeof value === 'object' ? value : {};
|
|
890
|
+
}
|
|
891
|
+
function readText(raw, keys) {
|
|
892
|
+
const body = asRecord(raw);
|
|
893
|
+
for (const key of keys) {
|
|
894
|
+
const value = body[key];
|
|
895
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
896
|
+
return value;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return '';
|
|
900
|
+
}
|
|
901
|
+
function readNumber(raw, keys) {
|
|
902
|
+
const body = asRecord(raw);
|
|
903
|
+
for (const key of keys) {
|
|
904
|
+
const value = body[key];
|
|
905
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
906
|
+
return value;
|
|
907
|
+
}
|
|
908
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
909
|
+
const numeric = Number(value);
|
|
910
|
+
if (Number.isFinite(numeric)) {
|
|
911
|
+
return numeric;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
function joinUrl(baseUrl, path) {
|
|
918
|
+
return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
|
|
919
|
+
}
|
|
920
|
+
class DesignbaseAccountAuthClient {
|
|
921
|
+
constructor(config) {
|
|
922
|
+
this.config = config;
|
|
923
|
+
if (!config.baseUrl.trim()) {
|
|
924
|
+
throw new Error('DesignbaseAccountAuthConfig.baseUrl is required.');
|
|
925
|
+
}
|
|
926
|
+
this.fetchFn = config.fetchFn ?? fetch;
|
|
927
|
+
}
|
|
928
|
+
async start(payload) {
|
|
929
|
+
const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.startPath || '/wp-json/custom/v1/figma/auth/start'), {
|
|
930
|
+
plugin_code: payload.pluginCode,
|
|
931
|
+
device_id: payload.deviceId,
|
|
932
|
+
return_url: payload.returnUrl || '',
|
|
933
|
+
});
|
|
934
|
+
return {
|
|
935
|
+
sessionId: readText(raw, ['session_id', 'sessionId']),
|
|
936
|
+
pollToken: readText(raw, ['poll_token', 'pollToken']),
|
|
937
|
+
authUrl: readText(raw, ['auth_url', 'authUrl']),
|
|
938
|
+
expiresIn: readNumber(raw, ['expires_in', 'expiresIn']) ?? 600,
|
|
939
|
+
pollInterval: readNumber(raw, ['poll_interval', 'pollInterval']) ?? 3,
|
|
940
|
+
raw,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
async poll(sessionId, pollToken) {
|
|
944
|
+
const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.pollPath || '/wp-json/custom/v1/figma/auth/poll'), {
|
|
945
|
+
session_id: sessionId,
|
|
946
|
+
poll_token: pollToken,
|
|
947
|
+
});
|
|
948
|
+
const status = readText(raw, ['status']) || 'pending';
|
|
949
|
+
const allowed = Boolean(asRecord(raw).allowed || status === 'active');
|
|
950
|
+
const tokens = asRecord(asRecord(raw).tokens);
|
|
951
|
+
const actions = asRecord(asRecord(raw).actions);
|
|
952
|
+
return {
|
|
953
|
+
status,
|
|
954
|
+
allowed,
|
|
955
|
+
pluginCode: readText(raw, ['plugin_code', 'pluginCode']),
|
|
956
|
+
productId: readText(raw, ['product_id', 'productId']) || undefined,
|
|
957
|
+
planTier: readText(raw, ['plan_tier', 'planTier']) || undefined,
|
|
958
|
+
licenseKey: readText(raw, ['license_key', 'licenseKey']) || undefined,
|
|
959
|
+
accessToken: readText(tokens, ['access_token', 'accessToken']) || undefined,
|
|
960
|
+
refreshToken: readText(tokens, ['refresh_token', 'refreshToken']) || undefined,
|
|
961
|
+
expiresIn: readNumber(tokens, ['expires_in', 'expiresIn']),
|
|
962
|
+
message: readText(raw, ['message']) || undefined,
|
|
963
|
+
actions,
|
|
964
|
+
raw,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async saveTokens(result) {
|
|
968
|
+
if (!this.config.storage || !result.accessToken) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const prefix = this.config.storagePrefix || 'designbase-account-auth';
|
|
972
|
+
await Promise.all([
|
|
973
|
+
this.config.storage.set(`${prefix}:accessToken`, result.accessToken),
|
|
974
|
+
this.config.storage.set(`${prefix}:refreshToken`, result.refreshToken || ''),
|
|
975
|
+
this.config.storage.set(`${prefix}:expiresAt`, Date.now() + ((result.expiresIn || 1800) * 1000)),
|
|
976
|
+
]);
|
|
977
|
+
}
|
|
978
|
+
async getAccessToken() {
|
|
979
|
+
if (!this.config.storage) {
|
|
980
|
+
return '';
|
|
981
|
+
}
|
|
982
|
+
const prefix = this.config.storagePrefix || 'designbase-account-auth';
|
|
983
|
+
const token = await this.config.storage.get(`${prefix}:accessToken`);
|
|
984
|
+
return typeof token === 'string' ? token : '';
|
|
985
|
+
}
|
|
986
|
+
async postJson(url, body) {
|
|
987
|
+
const timeoutMs = this.config.timeoutMs ?? 15000;
|
|
988
|
+
const abortController = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
|
|
989
|
+
const timeoutId = abortController ? setTimeout(() => abortController.abort(), timeoutMs) : undefined;
|
|
990
|
+
try {
|
|
991
|
+
const response = await this.fetchFn(url, {
|
|
992
|
+
method: 'POST',
|
|
993
|
+
headers: {
|
|
994
|
+
'Content-Type': 'application/json',
|
|
995
|
+
},
|
|
996
|
+
body: JSON.stringify(body),
|
|
997
|
+
signal: abortController?.signal,
|
|
998
|
+
});
|
|
999
|
+
if (!response.ok) {
|
|
1000
|
+
const text = await response.text();
|
|
1001
|
+
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
|
|
441
1002
|
}
|
|
442
1003
|
return await response.json();
|
|
443
1004
|
}
|
|
@@ -448,6 +1009,94 @@
|
|
|
448
1009
|
}
|
|
449
1010
|
}
|
|
450
1011
|
}
|
|
1012
|
+
class DesignbaseAccountLicenseTransport {
|
|
1013
|
+
constructor(config) {
|
|
1014
|
+
this.config = config;
|
|
1015
|
+
this.client = new DesignbaseAccountAuthClient(config);
|
|
1016
|
+
this.fetchFn = config.fetchFn ?? fetch;
|
|
1017
|
+
}
|
|
1018
|
+
async verify(payload) {
|
|
1019
|
+
return this.validate(payload);
|
|
1020
|
+
}
|
|
1021
|
+
async validate(payload) {
|
|
1022
|
+
const token = await this.resolveAccessToken();
|
|
1023
|
+
if (!token) {
|
|
1024
|
+
return {
|
|
1025
|
+
valid: false,
|
|
1026
|
+
message: 'Designbase account access token is missing.',
|
|
1027
|
+
raw: { status: 'not_logged_in' },
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
const url = new URL(joinUrl(this.config.baseUrl, this.config.statusPath || '/wp-json/custom/v1/product/status'));
|
|
1031
|
+
url.searchParams.set('plugin_code', payload.productId);
|
|
1032
|
+
const raw = await this.getJson(url.toString(), token);
|
|
1033
|
+
const status = readText(raw, ['status']);
|
|
1034
|
+
const allowed = Boolean(asRecord(raw).allowed || status === 'active');
|
|
1035
|
+
return {
|
|
1036
|
+
valid: allowed,
|
|
1037
|
+
message: readText(raw, ['message']) || undefined,
|
|
1038
|
+
productId: readText(raw, ['plugin_code', 'pluginCode']) || payload.productId,
|
|
1039
|
+
instanceId: payload.instanceId,
|
|
1040
|
+
activationLimit: readNumber(raw, ['activation_limit', 'activationLimit']) ?? 999999,
|
|
1041
|
+
activationUsage: readNumber(raw, ['activation_usage', 'activationUsage']) ?? 0,
|
|
1042
|
+
planTier: readText(raw, ['plan_tier', 'planTier']) || (allowed ? 'PRO' : 'FREE'),
|
|
1043
|
+
licenseKey: payload.licenseKey,
|
|
1044
|
+
raw,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
async deactivate(payload) {
|
|
1048
|
+
const token = await this.resolveAccessToken();
|
|
1049
|
+
if (!token) {
|
|
1050
|
+
return { success: true, message: 'Already signed out.' };
|
|
1051
|
+
}
|
|
1052
|
+
const raw = await this.postJson(joinUrl(this.config.baseUrl, this.config.deactivatePath || '/wp-json/custom/v1/product/deactivate'), {
|
|
1053
|
+
license_key: payload.licenseKey,
|
|
1054
|
+
device_id: payload.instanceId || payload.instanceName,
|
|
1055
|
+
plugin_code: payload.productId,
|
|
1056
|
+
}, token);
|
|
1057
|
+
const status = readText(raw, ['status']);
|
|
1058
|
+
return {
|
|
1059
|
+
success: Boolean(asRecord(raw).success || status === 'ok' || status === 'inactive'),
|
|
1060
|
+
message: readText(raw, ['message']) || undefined,
|
|
1061
|
+
raw,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
async resolveAccessToken() {
|
|
1065
|
+
if (this.config.getAccessToken) {
|
|
1066
|
+
const token = await this.config.getAccessToken();
|
|
1067
|
+
return typeof token === 'string' ? token : '';
|
|
1068
|
+
}
|
|
1069
|
+
return this.client.getAccessToken();
|
|
1070
|
+
}
|
|
1071
|
+
async getJson(url, token) {
|
|
1072
|
+
const response = await this.fetchFn(url, {
|
|
1073
|
+
method: 'GET',
|
|
1074
|
+
headers: {
|
|
1075
|
+
Authorization: `Bearer ${token}`,
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
if (!response.ok) {
|
|
1079
|
+
const text = await response.text();
|
|
1080
|
+
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
|
|
1081
|
+
}
|
|
1082
|
+
return response.json();
|
|
1083
|
+
}
|
|
1084
|
+
async postJson(url, body, token) {
|
|
1085
|
+
const response = await this.fetchFn(url, {
|
|
1086
|
+
method: 'POST',
|
|
1087
|
+
headers: {
|
|
1088
|
+
Authorization: `Bearer ${token}`,
|
|
1089
|
+
'Content-Type': 'application/json',
|
|
1090
|
+
},
|
|
1091
|
+
body: JSON.stringify(body),
|
|
1092
|
+
});
|
|
1093
|
+
if (!response.ok) {
|
|
1094
|
+
const text = await response.text();
|
|
1095
|
+
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
|
|
1096
|
+
}
|
|
1097
|
+
return response.json();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
451
1100
|
|
|
452
1101
|
class FigmaStorageAdapter {
|
|
453
1102
|
constructor(figma) {
|
|
@@ -489,6 +1138,8 @@
|
|
|
489
1138
|
class FigmaLicenseStatusManager {
|
|
490
1139
|
constructor(options) {
|
|
491
1140
|
this.options = options;
|
|
1141
|
+
this.featureAccessMessageType = 'feature-access-result';
|
|
1142
|
+
this.consumeUsageMessageType = 'usage-consume-result';
|
|
492
1143
|
this.messageTypes = {
|
|
493
1144
|
...DEFAULT_MESSAGE_TYPES,
|
|
494
1145
|
...(options.messageTypes || {}),
|
|
@@ -496,7 +1147,16 @@
|
|
|
496
1147
|
this.notifier = options.notifier;
|
|
497
1148
|
}
|
|
498
1149
|
async initialize() {
|
|
499
|
-
|
|
1150
|
+
if (!this.initializationPromise) {
|
|
1151
|
+
this.initializationPromise = (async () => {
|
|
1152
|
+
const state = await this.options.core.initialize();
|
|
1153
|
+
await this.postStatusUpdate(state);
|
|
1154
|
+
})().catch((error) => {
|
|
1155
|
+
this.initializationPromise = undefined;
|
|
1156
|
+
throw error;
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
await this.initializationPromise;
|
|
500
1160
|
}
|
|
501
1161
|
async handleMessage(pluginMessage) {
|
|
502
1162
|
if (!pluginMessage || typeof pluginMessage !== 'object') {
|
|
@@ -508,7 +1168,7 @@
|
|
|
508
1168
|
return false;
|
|
509
1169
|
}
|
|
510
1170
|
if (type === this.messageTypes.initialize) {
|
|
511
|
-
await this.
|
|
1171
|
+
await this.initialize();
|
|
512
1172
|
return true;
|
|
513
1173
|
}
|
|
514
1174
|
if (type === this.messageTypes.verifyLicense) {
|
|
@@ -520,6 +1180,17 @@
|
|
|
520
1180
|
await this.handleDeactivateLicense();
|
|
521
1181
|
return true;
|
|
522
1182
|
}
|
|
1183
|
+
if (type === 'check-feature-access') {
|
|
1184
|
+
const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
|
|
1185
|
+
await this.handleCheckFeatureAccess(featureKey);
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
if (type === 'consume-usage') {
|
|
1189
|
+
const amount = typeof message.amount === 'number' ? message.amount : 1;
|
|
1190
|
+
const featureKey = typeof message.featureKey === 'string' ? message.featureKey : undefined;
|
|
1191
|
+
await this.handleConsumeUsage(amount, featureKey);
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
523
1194
|
return false;
|
|
524
1195
|
}
|
|
525
1196
|
bindToOnMessage(fallbackHandler) {
|
|
@@ -535,14 +1206,19 @@
|
|
|
535
1206
|
try {
|
|
536
1207
|
const state = await this.options.core.verifyLicense(licenseKey);
|
|
537
1208
|
const access = this.options.core.resolveUsageAccess(state);
|
|
1209
|
+
const summary = await this.options.core.getLicenseSummary();
|
|
538
1210
|
this.postMessage({
|
|
539
1211
|
type: this.messageTypes.verifyResult,
|
|
540
1212
|
success: true,
|
|
541
1213
|
message: '라이선스가 검증되었습니다.',
|
|
542
1214
|
...state,
|
|
1215
|
+
...summary,
|
|
543
1216
|
usageLimit: this.options.core.getUsageLimit(),
|
|
544
1217
|
canUseFeature: access.allowed,
|
|
545
1218
|
remainingUsage: access.remaining,
|
|
1219
|
+
usageUsed: access.used,
|
|
1220
|
+
usageReset: access.reset,
|
|
1221
|
+
usageTier: access.tier,
|
|
546
1222
|
});
|
|
547
1223
|
await this.postStatusUpdate(state);
|
|
548
1224
|
}
|
|
@@ -555,20 +1231,26 @@
|
|
|
555
1231
|
message: mappedError.message,
|
|
556
1232
|
errorCode: mappedError.code,
|
|
557
1233
|
});
|
|
1234
|
+
await this.postStatusUpdate();
|
|
558
1235
|
}
|
|
559
1236
|
}
|
|
560
1237
|
async handleDeactivateLicense() {
|
|
561
1238
|
try {
|
|
562
1239
|
const state = await this.options.core.deactivateLicense();
|
|
563
1240
|
const access = this.options.core.resolveUsageAccess(state);
|
|
1241
|
+
const summary = await this.options.core.getLicenseSummary();
|
|
564
1242
|
this.postMessage({
|
|
565
1243
|
type: this.messageTypes.deactivateResult,
|
|
566
1244
|
success: true,
|
|
567
1245
|
message: '라이선스가 비활성화되었습니다.',
|
|
568
1246
|
...state,
|
|
1247
|
+
...summary,
|
|
569
1248
|
usageLimit: this.options.core.getUsageLimit(),
|
|
570
1249
|
canUseFeature: access.allowed,
|
|
571
1250
|
remainingUsage: access.remaining,
|
|
1251
|
+
usageUsed: access.used,
|
|
1252
|
+
usageReset: access.reset,
|
|
1253
|
+
usageTier: access.tier,
|
|
572
1254
|
});
|
|
573
1255
|
await this.postStatusUpdate(state);
|
|
574
1256
|
}
|
|
@@ -586,13 +1268,45 @@
|
|
|
586
1268
|
async postStatusUpdate(initialState) {
|
|
587
1269
|
const state = initialState ?? await this.options.core.getStatus();
|
|
588
1270
|
const access = this.options.core.resolveUsageAccess(state);
|
|
1271
|
+
const summary = await this.options.core.getLicenseSummary();
|
|
589
1272
|
this.postMessage({
|
|
590
1273
|
type: this.messageTypes.updateStatus,
|
|
591
1274
|
...state,
|
|
1275
|
+
...summary,
|
|
592
1276
|
usageLimit: this.options.core.getUsageLimit(),
|
|
593
1277
|
canUseFeature: access.allowed,
|
|
594
1278
|
remainingUsage: access.remaining,
|
|
1279
|
+
usageUsed: access.used,
|
|
1280
|
+
usageReset: access.reset,
|
|
1281
|
+
usageTier: access.tier,
|
|
1282
|
+
usageReason: access.reason,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
async handleCheckFeatureAccess(featureKey) {
|
|
1286
|
+
const state = await this.options.core.getStatus();
|
|
1287
|
+
const access = this.options.core.resolveUsageAccess(state, featureKey);
|
|
1288
|
+
const summary = await this.options.core.getLicenseSummary();
|
|
1289
|
+
this.postMessage({
|
|
1290
|
+
type: this.featureAccessMessageType,
|
|
1291
|
+
success: true,
|
|
1292
|
+
featureKey,
|
|
1293
|
+
...summary,
|
|
1294
|
+
...access,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
async handleConsumeUsage(amount, featureKey) {
|
|
1298
|
+
const result = await this.options.core.consumeUsage(amount, featureKey);
|
|
1299
|
+
const summary = await this.options.core.getLicenseSummary();
|
|
1300
|
+
this.postMessage({
|
|
1301
|
+
type: this.consumeUsageMessageType,
|
|
1302
|
+
success: result.access.allowed,
|
|
1303
|
+
featureKey,
|
|
1304
|
+
amount,
|
|
1305
|
+
...summary,
|
|
1306
|
+
...result.access,
|
|
1307
|
+
usageCount: result.state.usageCount,
|
|
595
1308
|
});
|
|
1309
|
+
await this.postStatusUpdate(result.state);
|
|
596
1310
|
}
|
|
597
1311
|
postMessage(message) {
|
|
598
1312
|
this.options.figma.ui.postMessage(message);
|
|
@@ -620,6 +1334,7 @@
|
|
|
620
1334
|
const notifier = options.notifier ?? new FigmaNotifierAdapter(figmaApi);
|
|
621
1335
|
const transport = options.transport ?? new FetchLicenseTransport({
|
|
622
1336
|
verifyUrl: options.config.endpoint,
|
|
1337
|
+
validateUrl: options.config.validateEndpoint,
|
|
623
1338
|
deactivateUrl: options.config.deactivateEndpoint,
|
|
624
1339
|
headers: options.config.requestHeaders,
|
|
625
1340
|
timeoutMs: options.config.requestTimeoutMs,
|
|
@@ -639,12 +1354,15 @@
|
|
|
639
1354
|
});
|
|
640
1355
|
}
|
|
641
1356
|
|
|
1357
|
+
exports.DesignbaseAccountAuthClient = DesignbaseAccountAuthClient;
|
|
1358
|
+
exports.DesignbaseAccountLicenseTransport = DesignbaseAccountLicenseTransport;
|
|
642
1359
|
exports.FetchLicenseTransport = FetchLicenseTransport;
|
|
643
1360
|
exports.FigmaLicenseStatusManager = FigmaLicenseStatusManager;
|
|
644
1361
|
exports.FigmaNotifierAdapter = FigmaNotifierAdapter;
|
|
645
1362
|
exports.FigmaStorageAdapter = FigmaStorageAdapter;
|
|
646
1363
|
exports.LicenseCore = LicenseCore;
|
|
647
1364
|
exports.LicenseCoreError = LicenseCoreError;
|
|
1365
|
+
exports.LicenseTransportHttpError = LicenseTransportHttpError;
|
|
648
1366
|
exports.createFigmaLicenseStatusManager = createFigmaLicenseStatusManager;
|
|
649
1367
|
exports.defaultNormalizeDeactivateResponse = defaultNormalizeDeactivateResponse;
|
|
650
1368
|
exports.defaultNormalizeVerifyResponse = defaultNormalizeVerifyResponse;
|