@kawanua/license-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +489 -0
- package/package.json +24 -0
package/index.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kawanua/license-sdk v1.0.0
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────
|
|
4
|
+
* Drop-in SDK untuk klien yang menggunakan lisensi Kawanua.
|
|
5
|
+
*
|
|
6
|
+
* Cara pakai:
|
|
7
|
+
* <script src="https://cdn.kawanua.id/sdk/license.min.js"></script>
|
|
8
|
+
* <script>
|
|
9
|
+
* KawanuaLicense.init({ token: 'eyJ...' });
|
|
10
|
+
* </script>
|
|
11
|
+
*
|
|
12
|
+
* Atau via ESM:
|
|
13
|
+
* import { KawanuaLicense } from '@kawanua/license-sdk';
|
|
14
|
+
* KawanuaLicense.init({ token: 'eyJ...' });
|
|
15
|
+
* ─────────────────────────────────────────────────────────────────
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
(function (global, factory) {
|
|
19
|
+
// UMD wrapper — support <script>, CommonJS, dan ESM
|
|
20
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
21
|
+
module.exports = factory();
|
|
22
|
+
} else if (typeof define === "function" && define.amd) {
|
|
23
|
+
define(factory);
|
|
24
|
+
} else {
|
|
25
|
+
global.KawanuaLicense = factory();
|
|
26
|
+
}
|
|
27
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
28
|
+
"use strict";
|
|
29
|
+
|
|
30
|
+
// ─── Konstanta ──────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Public key RS256 dari server lisensi Kawanua.
|
|
34
|
+
* Klien hanya bisa VERIFY — tidak bisa forge token baru.
|
|
35
|
+
* Ganti dengan public key aktual setelah generate keypair di server.
|
|
36
|
+
*/
|
|
37
|
+
const KAWANUA_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
38
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2a2rwplBQLzHPZe5TNJF
|
|
39
|
+
... (ganti dengan public key RS256 aktual dari server kamu) ...
|
|
40
|
+
-----END PUBLIC KEY-----`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* URL endpoint JWKS publik — alternatif lebih dinamis dari hardcode PEM.
|
|
44
|
+
* SDK akan fetch public key dari sini jika PEM tidak di-set.
|
|
45
|
+
* Endpoint ini harus tersedia di server Kawanua.
|
|
46
|
+
*/
|
|
47
|
+
const JWKS_URL = "https://lisensi.kawanua.workers.dev/.well-known/jwks.json";
|
|
48
|
+
|
|
49
|
+
/** Feature map per plan — single source of truth */
|
|
50
|
+
const PLAN_FEATURES = {
|
|
51
|
+
starter: {
|
|
52
|
+
remove_branding: false, // "Powered by Kawanua" tetap muncul
|
|
53
|
+
custom_domain: false,
|
|
54
|
+
api_access: false,
|
|
55
|
+
max_users: 5,
|
|
56
|
+
analytics: "basic",
|
|
57
|
+
white_label: false,
|
|
58
|
+
priority_support: false,
|
|
59
|
+
},
|
|
60
|
+
professional: {
|
|
61
|
+
remove_branding: true, // Branding disembunyikan
|
|
62
|
+
custom_domain: false,
|
|
63
|
+
api_access: true,
|
|
64
|
+
max_users: 50,
|
|
65
|
+
analytics: "advanced",
|
|
66
|
+
white_label: false,
|
|
67
|
+
priority_support: true,
|
|
68
|
+
},
|
|
69
|
+
enterprise: {
|
|
70
|
+
remove_branding: true,
|
|
71
|
+
custom_domain: true,
|
|
72
|
+
api_access: true,
|
|
73
|
+
max_users: Infinity,
|
|
74
|
+
analytics: "advanced",
|
|
75
|
+
white_label: true,
|
|
76
|
+
priority_support: true,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ─── State internal ─────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
let _state = {
|
|
83
|
+
initialized: false,
|
|
84
|
+
valid: false,
|
|
85
|
+
plan: null, // 'starter' | 'professional' | 'enterprise'
|
|
86
|
+
features: {}, // merged dari PLAN_FEATURES + JWT claims
|
|
87
|
+
license: null, // raw decoded payload
|
|
88
|
+
expiresAt: null,
|
|
89
|
+
_refreshTimer: null,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ─── Crypto: decode & verify JWT RS256 ─────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Decode JWT payload tanpa verify (hanya untuk read claims).
|
|
96
|
+
* JANGAN gunakan ini untuk security check.
|
|
97
|
+
*/
|
|
98
|
+
function _decodePayload(token) {
|
|
99
|
+
try {
|
|
100
|
+
const parts = token.split(".");
|
|
101
|
+
if (parts.length !== 3) throw new Error("Invalid JWT structure");
|
|
102
|
+
// Base64url → Base64 → JSON
|
|
103
|
+
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
104
|
+
const padded = base64.padEnd(
|
|
105
|
+
base64.length + ((4 - (base64.length % 4)) % 4),
|
|
106
|
+
"=",
|
|
107
|
+
);
|
|
108
|
+
return JSON.parse(atob(padded));
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Import public key RS256 dari PEM string ke CryptoKey.
|
|
116
|
+
* Menggunakan Web Crypto API (tersedia di semua browser modern).
|
|
117
|
+
*/
|
|
118
|
+
async function _importPublicKey(pem) {
|
|
119
|
+
const pemBody = pem
|
|
120
|
+
.replace(/-----BEGIN PUBLIC KEY-----/, "")
|
|
121
|
+
.replace(/-----END PUBLIC KEY-----/, "")
|
|
122
|
+
.replace(/\s+/g, "");
|
|
123
|
+
const der = Uint8Array.from(atob(pemBody), (c) => c.charCodeAt(0));
|
|
124
|
+
return crypto.subtle.importKey(
|
|
125
|
+
"spki",
|
|
126
|
+
der.buffer,
|
|
127
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
128
|
+
false,
|
|
129
|
+
["verify"],
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fetch public key dari JWKS endpoint.
|
|
135
|
+
* Fallback jika PEM belum di-hardcode (mode dinamis).
|
|
136
|
+
*/
|
|
137
|
+
async function _fetchPublicKeyFromJWKS(kid) {
|
|
138
|
+
const res = await fetch(JWKS_URL, { cache: "force-cache" });
|
|
139
|
+
const jwks = await res.json();
|
|
140
|
+
const jwk = kid ? jwks.keys.find((k) => k.kid === kid) : jwks.keys[0];
|
|
141
|
+
if (!jwk) throw new Error("No matching key found in JWKS");
|
|
142
|
+
return crypto.subtle.importKey(
|
|
143
|
+
"jwk",
|
|
144
|
+
jwk,
|
|
145
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
146
|
+
false,
|
|
147
|
+
["verify"],
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Verifikasi signature JWT dengan RS256.
|
|
153
|
+
* Returns true jika valid, false jika tidak.
|
|
154
|
+
*/
|
|
155
|
+
async function _verifySignature(token, publicKey) {
|
|
156
|
+
const [headerB64, payloadB64, sigB64] = token.split(".");
|
|
157
|
+
const message = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
158
|
+
const signature = Uint8Array.from(
|
|
159
|
+
atob(sigB64.replace(/-/g, "+").replace(/_/g, "/")),
|
|
160
|
+
(c) => c.charCodeAt(0),
|
|
161
|
+
);
|
|
162
|
+
return crypto.subtle.verify(
|
|
163
|
+
"RSASSA-PKCS1-v1_5",
|
|
164
|
+
publicKey,
|
|
165
|
+
signature,
|
|
166
|
+
message,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Full JWT verification pipeline:
|
|
172
|
+
* 1. Decode header untuk ambil kid & alg
|
|
173
|
+
* 2. Verify signature (RS256)
|
|
174
|
+
* 3. Cek expiry
|
|
175
|
+
* Returns decoded payload atau throws Error.
|
|
176
|
+
*/
|
|
177
|
+
async function _verifyToken(token) {
|
|
178
|
+
if (!token || typeof token !== "string") {
|
|
179
|
+
throw new Error("Token tidak valid.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Decode header
|
|
183
|
+
const headerRaw = token.split(".")[0];
|
|
184
|
+
const headerPad = headerRaw.replace(/-/g, "+").replace(/_/g, "/");
|
|
185
|
+
const header = JSON.parse(
|
|
186
|
+
atob(
|
|
187
|
+
headerPad.padEnd(
|
|
188
|
+
headerPad.length + ((4 - (headerPad.length % 4)) % 4),
|
|
189
|
+
"=",
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (header.alg !== "RS256") {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Algoritma JWT tidak didukung: ${header.alg}. Hanya RS256 yang diterima.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Import public key (prioritas: hardcode PEM → JWKS endpoint)
|
|
201
|
+
let publicKey;
|
|
202
|
+
const hasPem = KAWANUA_PUBLIC_KEY_PEM.includes("..."); // placeholder check
|
|
203
|
+
if (!hasPem) {
|
|
204
|
+
publicKey = await _importPublicKey(KAWANUA_PUBLIC_KEY_PEM);
|
|
205
|
+
} else {
|
|
206
|
+
publicKey = await _fetchPublicKeyFromJWKS(header.kid);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Verify signature
|
|
210
|
+
const isValid = await _verifySignature(token, publicKey);
|
|
211
|
+
if (!isValid) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"Signature JWT tidak valid. Token mungkin telah dimanipulasi.",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Decode payload
|
|
218
|
+
const payload = _decodePayload(token);
|
|
219
|
+
if (!payload) throw new Error("Payload JWT tidak dapat dibaca.");
|
|
220
|
+
|
|
221
|
+
// Cek expiry
|
|
222
|
+
const now = Math.floor(Date.now() / 1000);
|
|
223
|
+
if (payload.exp && payload.exp < now) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
"Lisensi telah kedaluwarsa. Silakan hubungi administrator.",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Cek not-before
|
|
230
|
+
if (payload.nbf && payload.nbf > now) {
|
|
231
|
+
throw new Error("Token belum aktif (nbf). Periksa waktu sistem kamu.");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return payload;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Feature resolution ─────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Merge feature defaults dari PLAN_FEATURES dengan override dari JWT claims.
|
|
241
|
+
* JWT claims selalu menang atas default plan (fine-grained control).
|
|
242
|
+
*/
|
|
243
|
+
function _resolveFeatures(plan, jwtFeatures = {}) {
|
|
244
|
+
const defaults = PLAN_FEATURES[plan] ?? PLAN_FEATURES["starter"];
|
|
245
|
+
return Object.freeze({ ...defaults, ...jwtFeatures });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── Branding injection ─────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
const BRANDING_ID = "__kawanua_powered_by";
|
|
251
|
+
const BRANDING_HTML = `
|
|
252
|
+
<a
|
|
253
|
+
id="${BRANDING_ID}"
|
|
254
|
+
href="https://kawanua.id"
|
|
255
|
+
target="_blank"
|
|
256
|
+
rel="noopener noreferrer"
|
|
257
|
+
style="
|
|
258
|
+
display: inline-flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
gap: 6px;
|
|
261
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
262
|
+
font-size: 11px;
|
|
263
|
+
color: #6b7280;
|
|
264
|
+
text-decoration: none;
|
|
265
|
+
padding: 4px 8px;
|
|
266
|
+
border: 1px solid #e5e7eb;
|
|
267
|
+
border-radius: 6px;
|
|
268
|
+
background: #f9fafb;
|
|
269
|
+
transition: opacity 0.2s ease;
|
|
270
|
+
position: fixed;
|
|
271
|
+
bottom: 12px;
|
|
272
|
+
right: 12px;
|
|
273
|
+
z-index: 9999;
|
|
274
|
+
"
|
|
275
|
+
onmouseover="this.style.opacity='0.7'"
|
|
276
|
+
onmouseout="this.style.opacity='1'"
|
|
277
|
+
>
|
|
278
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
279
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
280
|
+
</svg>
|
|
281
|
+
Powered by <strong style="color:#374151">Kawanua</strong>
|
|
282
|
+
</a>
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
function _injectBranding() {
|
|
286
|
+
if (document.getElementById(BRANDING_ID)) return;
|
|
287
|
+
const wrapper = document.createElement("div");
|
|
288
|
+
wrapper.innerHTML = BRANDING_HTML.trim();
|
|
289
|
+
document.body.appendChild(wrapper.firstElementChild);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function _removeBranding() {
|
|
293
|
+
const el = document.getElementById(BRANDING_ID);
|
|
294
|
+
if (el) el.remove();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function _applyBranding(features) {
|
|
298
|
+
// Pastikan DOM sudah ready
|
|
299
|
+
const apply = () => {
|
|
300
|
+
if (features.remove_branding) {
|
|
301
|
+
_removeBranding();
|
|
302
|
+
} else {
|
|
303
|
+
_injectBranding();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (document.readyState === "loading") {
|
|
308
|
+
document.addEventListener("DOMContentLoaded", apply, { once: true });
|
|
309
|
+
} else {
|
|
310
|
+
apply();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Auto-refresh ────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Schedule auto-refresh token sebelum expiry.
|
|
318
|
+
* onRefresh callback harus return Promise<string> berisi token baru.
|
|
319
|
+
*/
|
|
320
|
+
function _scheduleRefresh(expiresAt, onRefresh) {
|
|
321
|
+
if (_state._refreshTimer) clearTimeout(_state._refreshTimer);
|
|
322
|
+
if (!onRefresh || !expiresAt) return;
|
|
323
|
+
|
|
324
|
+
const msUntilExpiry = expiresAt * 1000 - Date.now();
|
|
325
|
+
// Refresh 5 menit sebelum expiry
|
|
326
|
+
const refreshIn = Math.max(msUntilExpiry - 5 * 60 * 1000, 0);
|
|
327
|
+
|
|
328
|
+
_state._refreshTimer = setTimeout(async () => {
|
|
329
|
+
try {
|
|
330
|
+
const newToken = await onRefresh();
|
|
331
|
+
if (newToken) await _initWithToken(newToken, onRefresh);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.warn("[KawanuaLicense] Auto-refresh gagal:", err.message);
|
|
334
|
+
}
|
|
335
|
+
}, refreshIn);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Core init ───────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
async function _initWithToken(token, onRefresh) {
|
|
341
|
+
// 1. Verify JWT
|
|
342
|
+
const payload = await _verifyToken(token);
|
|
343
|
+
|
|
344
|
+
// 2. Pastikan status aktif
|
|
345
|
+
if (!payload.valid || payload.status !== "active") {
|
|
346
|
+
throw new Error(
|
|
347
|
+
"Lisensi tidak aktif. Silakan perpanjang atau hubungi dukungan.",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 3. Ambil plan dari payload
|
|
352
|
+
const plan = payload.license?.plan ?? "starter";
|
|
353
|
+
|
|
354
|
+
// 4. Resolve features (default plan + override dari JWT)
|
|
355
|
+
const features = _resolveFeatures(plan, payload.features ?? {});
|
|
356
|
+
|
|
357
|
+
// 5. Update state
|
|
358
|
+
_state = {
|
|
359
|
+
..._state,
|
|
360
|
+
initialized: true,
|
|
361
|
+
valid: true,
|
|
362
|
+
plan,
|
|
363
|
+
features,
|
|
364
|
+
license: payload.license ?? null,
|
|
365
|
+
expiresAt: payload.exp ?? null,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// 6. Apply branding berdasarkan features
|
|
369
|
+
_applyBranding(features);
|
|
370
|
+
|
|
371
|
+
// 7. Schedule refresh otomatis
|
|
372
|
+
_scheduleRefresh(payload.exp, onRefresh);
|
|
373
|
+
|
|
374
|
+
return _state;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── Public API ─────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
const KawanuaLicense = {
|
|
380
|
+
/**
|
|
381
|
+
* Inisialisasi SDK dengan token JWT.
|
|
382
|
+
*
|
|
383
|
+
* @param {object} options
|
|
384
|
+
* @param {string} options.token — JWT token dari server lisensi Kawanua
|
|
385
|
+
* @param {Function} [options.onRefresh] — async () => string — callback untuk refresh token
|
|
386
|
+
* @param {Function} [options.onReady] — callback setelah init berhasil
|
|
387
|
+
* @param {Function} [options.onError] — callback jika init gagal
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* KawanuaLicense.init({
|
|
391
|
+
* token: 'eyJ...',
|
|
392
|
+
* onRefresh: async () => {
|
|
393
|
+
* const res = await fetch('/api/license/token');
|
|
394
|
+
* const { token } = await res.json();
|
|
395
|
+
* return token;
|
|
396
|
+
* },
|
|
397
|
+
* onReady: (state) => console.log('Plan:', state.plan),
|
|
398
|
+
* onError: (err) => console.error('Lisensi error:', err.message),
|
|
399
|
+
* });
|
|
400
|
+
*/
|
|
401
|
+
async init({ token, onRefresh, onReady, onError } = {}) {
|
|
402
|
+
try {
|
|
403
|
+
const state = await _initWithToken(token, onRefresh);
|
|
404
|
+
onReady?.(state);
|
|
405
|
+
return state;
|
|
406
|
+
} catch (err) {
|
|
407
|
+
_state.initialized = true;
|
|
408
|
+
_state.valid = false;
|
|
409
|
+
// Fallback: jika error, tampilkan branding (gagal safe)
|
|
410
|
+
_applyBranding({ remove_branding: false });
|
|
411
|
+
onError?.(err);
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Cek apakah fitur tertentu aktif.
|
|
418
|
+
* Harus dipanggil setelah init().
|
|
419
|
+
*
|
|
420
|
+
* @param {string} featureName — nama fitur sesuai PLAN_FEATURES
|
|
421
|
+
* @returns {boolean|number|string}
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* if (KawanuaLicense.can('api_access')) {
|
|
425
|
+
* // tampilkan tombol API
|
|
426
|
+
* }
|
|
427
|
+
*/
|
|
428
|
+
can(featureName) {
|
|
429
|
+
if (!_state.initialized) {
|
|
430
|
+
console.warn(
|
|
431
|
+
"[KawanuaLicense] SDK belum diinisialisasi. Panggil init() terlebih dahulu.",
|
|
432
|
+
);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
return _state.features[featureName] ?? false;
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Dapatkan nilai numerik/string sebuah fitur.
|
|
440
|
+
* Berguna untuk limit seperti max_users.
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* const maxUsers = KawanuaLicense.get('max_users'); // 5, 50, atau Infinity
|
|
444
|
+
*/
|
|
445
|
+
get(featureName) {
|
|
446
|
+
return _state.features[featureName] ?? null;
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Dapatkan plan aktif saat ini.
|
|
451
|
+
* @returns {'starter'|'professional'|'enterprise'|null}
|
|
452
|
+
*/
|
|
453
|
+
getPlan() {
|
|
454
|
+
return _state.plan;
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Dapatkan full state (readonly snapshot).
|
|
459
|
+
*/
|
|
460
|
+
getState() {
|
|
461
|
+
return { ..._state, features: { ..._state.features } };
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Cek apakah lisensi valid dan aktif.
|
|
466
|
+
*/
|
|
467
|
+
isValid() {
|
|
468
|
+
return _state.initialized && _state.valid;
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Destroy: bersihkan timer, reset state.
|
|
473
|
+
*/
|
|
474
|
+
destroy() {
|
|
475
|
+
if (_state._refreshTimer) clearTimeout(_state._refreshTimer);
|
|
476
|
+
_state = {
|
|
477
|
+
initialized: false,
|
|
478
|
+
valid: false,
|
|
479
|
+
plan: null,
|
|
480
|
+
features: {},
|
|
481
|
+
license: null,
|
|
482
|
+
expiresAt: null,
|
|
483
|
+
_refreshTimer: null,
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
return KawanuaLicense;
|
|
489
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kawanua/license-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drop-in SDK untuk klien yang menggunakan lisensi Kawanua.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kawanua",
|
|
7
|
+
"license"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/kawanuaid/license-sdk#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/kawanuaid/license-sdk/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/kawanuaid/license-sdk.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"author": "Kawanua Indo Digital",
|
|
19
|
+
"type": "commonjs",
|
|
20
|
+
"main": "index.js",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
23
|
+
}
|
|
24
|
+
}
|