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