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