@tertaut/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -0
- package/dist/ai.d.ts +29 -0
- package/dist/ai.js +89 -0
- package/dist/chrome.d.ts +22 -0
- package/dist/chrome.js +70 -0
- package/dist/client.d.ts +124 -0
- package/dist/client.js +124 -0
- package/dist/desktop.d.ts +25 -0
- package/dist/desktop.js +57 -0
- package/dist/guard.d.ts +58 -0
- package/dist/guard.js +255 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/jwt.d.ts +7 -0
- package/dist/jwt.js +61 -0
- package/dist/storage.d.ts +23 -0
- package/dist/storage.js +189 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.js +1 -0
- package/dist/webhooks.d.ts +24 -0
- package/dist/webhooks.js +43 -0
- package/package.json +54 -0
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ActivationResult, LicenseCheckResult, ProtectOptions, SdkOptions } from './types.js';
|
|
2
|
+
import type { StoredLicenseData } from './storage.js';
|
|
3
|
+
import type { ActivateInput, TertautClient } from './client.js';
|
|
4
|
+
export declare class LicenseGuard {
|
|
5
|
+
private client;
|
|
6
|
+
private opts;
|
|
7
|
+
private storage;
|
|
8
|
+
private gracePeriodMs;
|
|
9
|
+
constructor(client: TertautClient, opts: SdkOptions);
|
|
10
|
+
/**
|
|
11
|
+
* Mengambil data lisensi yang tersimpan di storage lokal.
|
|
12
|
+
*/
|
|
13
|
+
getStored(): Promise<StoredLicenseData | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Cek cepat apakah software sudah diaktivasi.
|
|
16
|
+
*/
|
|
17
|
+
isActivated(): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* 1 BARIS KODE: Aktivasi lisensi software dan langsung simpan ke storage lokal.
|
|
20
|
+
* Otomatis mengunduh JWKS dan token offline untuk perlindungan tanpa jaringan.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const res = await tertaut.activate('TRT-ABCD-EFGH-IJKL')
|
|
24
|
+
* console.log('Aktivasi berhasil:', res.productName)
|
|
25
|
+
*/
|
|
26
|
+
activate(key: string, input?: Partial<ActivateInput>): Promise<ActivationResult>;
|
|
27
|
+
/**
|
|
28
|
+
* 1 BARIS KODE: Cek status lisensi dengan fallback offline otomatis (RS256).
|
|
29
|
+
* Otomatis membaca storage lokal jika key tidak diberikan.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const check = await tertaut.guard()
|
|
33
|
+
* if (!check.valid) {
|
|
34
|
+
* console.error('Lisensi tidak sah:', check.reason)
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
guard(options?: ProtectOptions): Promise<LicenseCheckResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Memproteksi software — melempar error jika lisensi tidak sah.
|
|
40
|
+
* Sangat praktis untuk middleware / guard awal aplikasi.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* await tertaut.protect({
|
|
44
|
+
* onInvalid: (res) => alert('Aplikasi terkunci: ' + res.reason)
|
|
45
|
+
* })
|
|
46
|
+
*/
|
|
47
|
+
protect(options?: ProtectOptions): Promise<LicenseCheckResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Melepas lisensi dari perangkat ini dan membersihkan cache lokal.
|
|
50
|
+
*/
|
|
51
|
+
deactivate(key?: string): Promise<{
|
|
52
|
+
deactivated: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Verifikasi offline token RS256 lokal + pengecekan grace period.
|
|
56
|
+
*/
|
|
57
|
+
private verifyOfflineGrace;
|
|
58
|
+
}
|
package/dist/guard.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createUniversalStorage } from './storage.js';
|
|
2
|
+
import { computeFingerprint, verifyOfflineLocally } from './desktop.js';
|
|
3
|
+
export class LicenseGuard {
|
|
4
|
+
client;
|
|
5
|
+
opts;
|
|
6
|
+
storage;
|
|
7
|
+
gracePeriodMs;
|
|
8
|
+
constructor(client, opts) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
this.storage = opts.storage || createUniversalStorage();
|
|
12
|
+
this.gracePeriodMs = opts.offlineGracePeriodMs ?? 3 * 24 * 60 * 60 * 1000; // 3 hari default
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Mengambil data lisensi yang tersimpan di storage lokal.
|
|
16
|
+
*/
|
|
17
|
+
async getStored() {
|
|
18
|
+
return this.storage.load(this.opts.productSlug);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cek cepat apakah software sudah diaktivasi.
|
|
22
|
+
*/
|
|
23
|
+
async isActivated() {
|
|
24
|
+
const stored = await this.getStored();
|
|
25
|
+
return !!(stored && stored.key);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 1 BARIS KODE: Aktivasi lisensi software dan langsung simpan ke storage lokal.
|
|
29
|
+
* Otomatis mengunduh JWKS dan token offline untuk perlindungan tanpa jaringan.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const res = await tertaut.activate('TRT-ABCD-EFGH-IJKL')
|
|
33
|
+
* console.log('Aktivasi berhasil:', res.productName)
|
|
34
|
+
*/
|
|
35
|
+
async activate(key, input = {}) {
|
|
36
|
+
const cleanKey = key.trim().toUpperCase();
|
|
37
|
+
const productSlug = input.productSlug || this.opts.productSlug;
|
|
38
|
+
const deviceFingerprint = input.deviceFingerprint || (await computeFingerprint());
|
|
39
|
+
// 1. Panggil server aktivasi
|
|
40
|
+
const res = await this.client.license.activate({
|
|
41
|
+
key: cleanKey,
|
|
42
|
+
productSlug,
|
|
43
|
+
deviceFingerprint,
|
|
44
|
+
activationType: input.activationType || 'MACHINE_BIND',
|
|
45
|
+
deviceName: input.deviceName,
|
|
46
|
+
domain: input.domain,
|
|
47
|
+
chromeIdentityId: input.chromeIdentityId,
|
|
48
|
+
});
|
|
49
|
+
// 2. Unduh public JWKS sesaat setelah aktivasi untuk verifikasi offline di masa mendatang
|
|
50
|
+
let jwks = null;
|
|
51
|
+
try {
|
|
52
|
+
jwks = await this.client.license.jwks();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* abaikan bila server JWKS sementara tidak merespon */
|
|
56
|
+
}
|
|
57
|
+
// 3. Simpan ke storage lokal
|
|
58
|
+
await this.storage.save({
|
|
59
|
+
key: cleanKey,
|
|
60
|
+
productSlug: res.productSlug || productSlug,
|
|
61
|
+
offlineToken: res.offlineToken || undefined,
|
|
62
|
+
jwks,
|
|
63
|
+
lastValidatedAt: new Date().toISOString(),
|
|
64
|
+
deviceFingerprint,
|
|
65
|
+
deviceName: input.deviceName,
|
|
66
|
+
metadata: {
|
|
67
|
+
productName: res.productName,
|
|
68
|
+
expiresAt: res.expiresAt,
|
|
69
|
+
maxDevices: res.maxDevices,
|
|
70
|
+
devicesUsed: res.devicesUsed,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return res;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 1 BARIS KODE: Cek status lisensi dengan fallback offline otomatis (RS256).
|
|
77
|
+
* Otomatis membaca storage lokal jika key tidak diberikan.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* const check = await tertaut.guard()
|
|
81
|
+
* if (!check.valid) {
|
|
82
|
+
* console.error('Lisensi tidak sah:', check.reason)
|
|
83
|
+
* }
|
|
84
|
+
*/
|
|
85
|
+
async guard(options = {}) {
|
|
86
|
+
const productSlug = options.productSlug || this.opts.productSlug;
|
|
87
|
+
const stored = await this.storage.load(productSlug);
|
|
88
|
+
const key = (options.key || stored?.key || this.opts.licenseKey || '').trim().toUpperCase();
|
|
89
|
+
if (!key && !stored?.offlineToken) {
|
|
90
|
+
return {
|
|
91
|
+
valid: false,
|
|
92
|
+
status: 'NOT_ACTIVATED',
|
|
93
|
+
reason: 'Lisensi belum diaktivasi pada perangkat ini.',
|
|
94
|
+
offline: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const fingerprint = stored?.deviceFingerprint || (await computeFingerprint());
|
|
98
|
+
// JIKA TIDAK MEMAKSA REMOTE DAN SEDANG OFFLINE ATAU INGIN CEK CEPAT
|
|
99
|
+
if (!options.forceRemote && typeof navigator !== 'undefined' && navigator.onLine === false && stored?.offlineToken) {
|
|
100
|
+
return this.verifyOfflineGrace(stored, key);
|
|
101
|
+
}
|
|
102
|
+
// VALIDASI ONLINE KE SERVER TERTAUT
|
|
103
|
+
try {
|
|
104
|
+
const res = await this.client.license.validate({
|
|
105
|
+
key: key || undefined,
|
|
106
|
+
productSlug,
|
|
107
|
+
deviceFingerprint: fingerprint,
|
|
108
|
+
offlineToken: stored?.offlineToken,
|
|
109
|
+
});
|
|
110
|
+
if (res.valid) {
|
|
111
|
+
// Perbarui timestamp validasi terakhir di cache
|
|
112
|
+
if (stored) {
|
|
113
|
+
stored.lastValidatedAt = new Date().toISOString();
|
|
114
|
+
if (res.expiresAt !== undefined) {
|
|
115
|
+
stored.metadata = { ...stored.metadata, expiresAt: res.expiresAt };
|
|
116
|
+
}
|
|
117
|
+
await this.storage.save(stored);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
valid: true,
|
|
121
|
+
status: res.status || 'ACTIVE',
|
|
122
|
+
key: res.key || key,
|
|
123
|
+
productName: res.productName || stored?.metadata?.productName,
|
|
124
|
+
productSlug: res.productSlug || productSlug,
|
|
125
|
+
expiresAt: res.expiresAt,
|
|
126
|
+
offline: false,
|
|
127
|
+
claims: res.claims,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Server mengembalikan bahwa lisensi tidak valid / revoked
|
|
131
|
+
return {
|
|
132
|
+
valid: false,
|
|
133
|
+
status: res.status || 'INVALID',
|
|
134
|
+
key,
|
|
135
|
+
reason: 'Lisensi tidak sah, dibatalkan, atau telah melebihi batas perangkat.',
|
|
136
|
+
offline: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (networkError) {
|
|
140
|
+
// JARINGAN TERPUTUS / SERVER TIMEOUT / OFFLINE
|
|
141
|
+
// Gunakan token offline RS256 yang tersimpan dengan aman
|
|
142
|
+
if (stored?.offlineToken) {
|
|
143
|
+
return this.verifyOfflineGrace(stored, key);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
valid: false,
|
|
147
|
+
status: 'INVALID',
|
|
148
|
+
key,
|
|
149
|
+
reason: `Gagal memverifikasi lisensi dan tidak ada cache offline: ${networkError?.message || 'Koneksi gagal'}`,
|
|
150
|
+
offline: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Memproteksi software — melempar error jika lisensi tidak sah.
|
|
156
|
+
* Sangat praktis untuk middleware / guard awal aplikasi.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* await tertaut.protect({
|
|
160
|
+
* onInvalid: (res) => alert('Aplikasi terkunci: ' + res.reason)
|
|
161
|
+
* })
|
|
162
|
+
*/
|
|
163
|
+
async protect(options = {}) {
|
|
164
|
+
const result = await this.guard(options);
|
|
165
|
+
if (!result.valid) {
|
|
166
|
+
if (options.onInvalid) {
|
|
167
|
+
await options.onInvalid(result);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
throw new Error(`[Tertaut] Lisensi Tidak Sah (${result.status}): ${result.reason || 'Proteksi aktif'}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Melepas lisensi dari perangkat ini dan membersihkan cache lokal.
|
|
177
|
+
*/
|
|
178
|
+
async deactivate(key) {
|
|
179
|
+
const productSlug = this.opts.productSlug;
|
|
180
|
+
const stored = await this.storage.load(productSlug);
|
|
181
|
+
const activeKey = (key || stored?.key || this.opts.licenseKey || '').trim().toUpperCase();
|
|
182
|
+
if (activeKey) {
|
|
183
|
+
try {
|
|
184
|
+
const fingerprint = stored?.deviceFingerprint || (await computeFingerprint());
|
|
185
|
+
await this.client.license.deactivate({
|
|
186
|
+
key: activeKey,
|
|
187
|
+
deviceFingerprint: fingerprint,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* lanjut bersihkan storage lokal */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await this.storage.clear(productSlug);
|
|
195
|
+
return { deactivated: true };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Verifikasi offline token RS256 lokal + pengecekan grace period.
|
|
199
|
+
*/
|
|
200
|
+
async verifyOfflineGrace(stored, key) {
|
|
201
|
+
if (!stored.offlineToken) {
|
|
202
|
+
return {
|
|
203
|
+
valid: false,
|
|
204
|
+
status: 'INVALID',
|
|
205
|
+
key,
|
|
206
|
+
reason: 'Tidak ditemukan token offline.',
|
|
207
|
+
offline: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const offlineRes = await verifyOfflineLocally(stored.offlineToken, stored.jwks || (() => this.client.license.jwks()));
|
|
212
|
+
if (!offlineRes.valid) {
|
|
213
|
+
return {
|
|
214
|
+
valid: false,
|
|
215
|
+
status: 'EXPIRED',
|
|
216
|
+
key,
|
|
217
|
+
reason: 'Token offline telah kedaluwarsa atau tanda tangan RS256 tidak cocok.',
|
|
218
|
+
offline: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// Cek apakah toleransi offline grace period terlampaui
|
|
222
|
+
const lastVal = new Date(stored.lastValidatedAt).getTime();
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (now - lastVal > this.gracePeriodMs) {
|
|
225
|
+
const days = Math.round(this.gracePeriodMs / (24 * 60 * 60 * 1000));
|
|
226
|
+
return {
|
|
227
|
+
valid: false,
|
|
228
|
+
status: 'EXPIRED',
|
|
229
|
+
key,
|
|
230
|
+
reason: `Batas toleransi offline (${days} hari) telah berakhir. Harap sambungkan ke internet untuk menyegarkan lisensi.`,
|
|
231
|
+
offline: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
valid: true,
|
|
236
|
+
status: 'OFFLINE_GRACE',
|
|
237
|
+
key,
|
|
238
|
+
productName: stored.metadata?.productName || offlineRes.claims.productName,
|
|
239
|
+
productSlug: stored.productSlug || offlineRes.claims.productSlug,
|
|
240
|
+
expiresAt: offlineRes.expiresAt,
|
|
241
|
+
offline: true,
|
|
242
|
+
claims: offlineRes.claims,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
return {
|
|
247
|
+
valid: false,
|
|
248
|
+
status: 'INVALID',
|
|
249
|
+
key,
|
|
250
|
+
reason: `Verifikasi offline gagal: ${err?.message || 'Kesalahan kriptografi'}`,
|
|
251
|
+
offline: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export type { ActivateInput, ValidateInput, DeactivateInput, ChatInput, TertautClient, } from './client.js';
|
|
3
|
+
export { createClient } from './client.js';
|
|
4
|
+
export { LicenseGuard } from './guard.js';
|
|
5
|
+
export { AiClient } from './ai.js';
|
|
6
|
+
export { webhooks, type WebhookVerifyOptions } from './webhooks.js';
|
|
7
|
+
export { createUniversalStorage, type LicenseStorage, type StoredLicenseData, } from './storage.js';
|
|
8
|
+
export { buildDeepLink, computeFingerprint, verifyOfflineLocally, } from './desktop.js';
|
|
9
|
+
export { getIdentityId, saveToSyncStorage, loadFromSyncStorage, persistJournal, getExtensionId, } from './chrome.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export { createClient } from './client.js';
|
|
3
|
+
export { LicenseGuard } from './guard.js';
|
|
4
|
+
export { AiClient } from './ai.js';
|
|
5
|
+
export { webhooks } from './webhooks.js';
|
|
6
|
+
export { createUniversalStorage, } from './storage.js';
|
|
7
|
+
export { buildDeepLink, computeFingerprint, verifyOfflineLocally, } from './desktop.js';
|
|
8
|
+
export { getIdentityId, saveToSyncStorage, loadFromSyncStorage, persistJournal, getExtensionId, } from './chrome.js';
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JwksResponse, OfflineVerifyResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Verifikasi offline RS256 berbasis WebCrypto — TANPA jaringan.
|
|
4
|
+
* Mencocokkan token (JWT RS256 bertanda "tertaut") terhadap JWKS publik
|
|
5
|
+
* yang didapat dari GET /api/licenses/.well-known/jwks sesaat sebelum offline.
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifyOfflineToken(token: string, jwks: JwksResponse): Promise<OfflineVerifyResult>;
|
package/dist/jwt.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function base64UrlToBytes(part) {
|
|
2
|
+
const b64 = part.replace(/-/g, '+').replace(/_/g, '/');
|
|
3
|
+
const pad = part.length % 4 === 0 ? '' : '='.repeat(4 - (part.length % 4));
|
|
4
|
+
const bin = atob(b64 + pad);
|
|
5
|
+
const bytes = new Uint8Array(bin.length);
|
|
6
|
+
for (let i = 0; i < bin.length; i++)
|
|
7
|
+
bytes[i] = bin.charCodeAt(i);
|
|
8
|
+
return bytes;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Verifikasi offline RS256 berbasis WebCrypto — TANPA jaringan.
|
|
12
|
+
* Mencocokkan token (JWT RS256 bertanda "tertaut") terhadap JWKS publik
|
|
13
|
+
* yang didapat dari GET /api/licenses/.well-known/jwks sesaat sebelum offline.
|
|
14
|
+
*/
|
|
15
|
+
export async function verifyOfflineToken(token, jwks) {
|
|
16
|
+
const parts = token.split('.');
|
|
17
|
+
if (parts.length !== 3) {
|
|
18
|
+
return { valid: false, status: 'EXPIRED', claims: {}, expiresAt: null };
|
|
19
|
+
}
|
|
20
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
21
|
+
let header;
|
|
22
|
+
let claims;
|
|
23
|
+
try {
|
|
24
|
+
header = JSON.parse(new TextDecoder().decode(base64UrlToBytes(headerB64)));
|
|
25
|
+
claims = JSON.parse(new TextDecoder().decode(base64UrlToBytes(payloadB64)));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { valid: false, status: 'EXPIRED', claims: {}, expiresAt: null };
|
|
29
|
+
}
|
|
30
|
+
if (header.alg !== 'RS256') {
|
|
31
|
+
return { valid: false, status: 'EXPIRED', claims, expiresAt: null };
|
|
32
|
+
}
|
|
33
|
+
const key = jwks.keys.find((k) => k.kid === header.kid);
|
|
34
|
+
if (!key) {
|
|
35
|
+
return { valid: false, status: 'EXPIRED', claims, expiresAt: null };
|
|
36
|
+
}
|
|
37
|
+
const subtle = globalThis.crypto?.subtle;
|
|
38
|
+
if (!subtle) {
|
|
39
|
+
throw new Error('WebCrypto (crypto.subtle) tidak tersedia di environment ini');
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const cryptoKey = await subtle.importKey('jwk', { kty: key.kty, kid: key.kid, n: key.n, e: key.e, alg: 'RS256', use: 'sig' }, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
|
|
43
|
+
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
44
|
+
const signature = base64UrlToBytes(signatureB64);
|
|
45
|
+
const valid = await subtle.verify('RSASSA-PKCS1-v1_5', cryptoKey, signature, data);
|
|
46
|
+
if (!valid) {
|
|
47
|
+
return { valid: false, status: 'EXPIRED', claims, expiresAt: null };
|
|
48
|
+
}
|
|
49
|
+
const exp = typeof claims.exp === 'number' ? claims.exp : Number(claims.exp) || 0;
|
|
50
|
+
const hasExpired = exp !== 0 && Date.now() > exp * 1000;
|
|
51
|
+
return {
|
|
52
|
+
valid: true,
|
|
53
|
+
status: hasExpired ? 'EXPIRED' : 'ACTIVE',
|
|
54
|
+
claims,
|
|
55
|
+
expiresAt: hasExpired || exp === 0 ? null : new Date(exp * 1000).toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { valid: false, status: 'EXPIRED', claims, expiresAt: null };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Storage Adapter untuk Tertaut SDK.
|
|
3
|
+
* Otomatis mendeteksi lingkungan: Browser, Chrome Extension, Node.js, Electron, atau CLI.
|
|
4
|
+
*/
|
|
5
|
+
export interface StoredLicenseData {
|
|
6
|
+
key: string;
|
|
7
|
+
productSlug?: string;
|
|
8
|
+
offlineToken?: string;
|
|
9
|
+
jwks?: any;
|
|
10
|
+
lastValidatedAt: string;
|
|
11
|
+
deviceFingerprint?: string;
|
|
12
|
+
deviceName?: string;
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface LicenseStorage {
|
|
16
|
+
load(productSlug?: string): Promise<StoredLicenseData | null>;
|
|
17
|
+
save(data: StoredLicenseData): Promise<void>;
|
|
18
|
+
clear(productSlug?: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the most appropriate storage engine for the current runtime environment.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createUniversalStorage(): LicenseStorage;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Storage Adapter untuk Tertaut SDK.
|
|
3
|
+
* Otomatis mendeteksi lingkungan: Browser, Chrome Extension, Node.js, Electron, atau CLI.
|
|
4
|
+
*/
|
|
5
|
+
// In-memory fallback
|
|
6
|
+
let memoryStorage = {};
|
|
7
|
+
function getStorageKey(productSlug) {
|
|
8
|
+
return productSlug ? `tertaut_license_${productSlug}` : 'tertaut_license_default';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 1. Storage untuk Web Browser (localStorage)
|
|
12
|
+
*/
|
|
13
|
+
class BrowserStorage {
|
|
14
|
+
async load(productSlug) {
|
|
15
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const raw = window.localStorage.getItem(getStorageKey(productSlug));
|
|
19
|
+
return raw ? JSON.parse(raw) : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async save(data) {
|
|
26
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
window.localStorage.setItem(getStorageKey(data.productSlug), JSON.stringify(data));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore quota errors */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async clear(productSlug) {
|
|
36
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
window.localStorage.removeItem(getStorageKey(productSlug));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 2. Storage untuk Chrome Extension (chrome.storage.sync / local)
|
|
48
|
+
*/
|
|
49
|
+
class ChromeStorage {
|
|
50
|
+
getArea() {
|
|
51
|
+
const ch = globalThis.chrome;
|
|
52
|
+
return ch?.storage?.sync || ch?.storage?.local || null;
|
|
53
|
+
}
|
|
54
|
+
async load(productSlug) {
|
|
55
|
+
const area = this.getArea();
|
|
56
|
+
if (!area)
|
|
57
|
+
return null;
|
|
58
|
+
const key = getStorageKey(productSlug);
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
area.get(key, (items) => {
|
|
61
|
+
resolve(items && items[key] ? items[key] : null);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async save(data) {
|
|
66
|
+
const area = this.getArea();
|
|
67
|
+
if (!area)
|
|
68
|
+
return;
|
|
69
|
+
const key = getStorageKey(data.productSlug);
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
area.set({ [key]: data }, () => resolve());
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async clear(productSlug) {
|
|
75
|
+
const area = this.getArea();
|
|
76
|
+
if (!area)
|
|
77
|
+
return;
|
|
78
|
+
const key = getStorageKey(productSlug);
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
area.remove(key, () => resolve());
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 3. Storage untuk Node.js / Electron / CLI (File JSON di ~/.tertaut/)
|
|
86
|
+
*/
|
|
87
|
+
class NodeFileStorage {
|
|
88
|
+
async getFs() {
|
|
89
|
+
try {
|
|
90
|
+
// Dynamic import to prevent bundler errors in browser environments
|
|
91
|
+
const fs = await import('node:fs');
|
|
92
|
+
const path = await import('node:path');
|
|
93
|
+
const os = await import('node:os');
|
|
94
|
+
return { fs, path, os };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async getFilePath(productSlug) {
|
|
101
|
+
const node = await this.getFs();
|
|
102
|
+
if (!node)
|
|
103
|
+
return null;
|
|
104
|
+
const home = node.os.homedir();
|
|
105
|
+
const dir = node.path.join(home, '.tertaut');
|
|
106
|
+
try {
|
|
107
|
+
if (!node.fs.existsSync(dir)) {
|
|
108
|
+
node.fs.mkdirSync(dir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const safeName = (productSlug || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
115
|
+
return node.path.join(dir, `license_${safeName}.json`);
|
|
116
|
+
}
|
|
117
|
+
async load(productSlug) {
|
|
118
|
+
try {
|
|
119
|
+
const node = await this.getFs();
|
|
120
|
+
const filePath = await this.getFilePath(productSlug);
|
|
121
|
+
if (!node || !filePath || !node.fs.existsSync(filePath))
|
|
122
|
+
return null;
|
|
123
|
+
const raw = node.fs.readFileSync(filePath, 'utf-8');
|
|
124
|
+
return JSON.parse(raw);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async save(data) {
|
|
131
|
+
try {
|
|
132
|
+
const node = await this.getFs();
|
|
133
|
+
const filePath = await this.getFilePath(data.productSlug);
|
|
134
|
+
if (!node || !filePath)
|
|
135
|
+
return;
|
|
136
|
+
node.fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* fallback ke memory */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async clear(productSlug) {
|
|
143
|
+
try {
|
|
144
|
+
const node = await this.getFs();
|
|
145
|
+
const filePath = await this.getFilePath(productSlug);
|
|
146
|
+
if (!node || !filePath || !node.fs.existsSync(filePath))
|
|
147
|
+
return;
|
|
148
|
+
node.fs.unlinkSync(filePath);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* ignore */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 4. In-Memory Storage (Fallback aman)
|
|
157
|
+
*/
|
|
158
|
+
class MemoryStorage {
|
|
159
|
+
async load(productSlug) {
|
|
160
|
+
return memoryStorage[getStorageKey(productSlug)] || null;
|
|
161
|
+
}
|
|
162
|
+
async save(data) {
|
|
163
|
+
memoryStorage[getStorageKey(data.productSlug)] = data;
|
|
164
|
+
}
|
|
165
|
+
async clear(productSlug) {
|
|
166
|
+
delete memoryStorage[getStorageKey(productSlug)];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Resolves the most appropriate storage engine for the current runtime environment.
|
|
171
|
+
*/
|
|
172
|
+
export function createUniversalStorage() {
|
|
173
|
+
// 1. Chrome Extension environment
|
|
174
|
+
const ch = globalThis.chrome;
|
|
175
|
+
if (ch?.storage?.sync || ch?.storage?.local) {
|
|
176
|
+
return new ChromeStorage();
|
|
177
|
+
}
|
|
178
|
+
// 2. Browser environment
|
|
179
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
180
|
+
return new BrowserStorage();
|
|
181
|
+
}
|
|
182
|
+
// 3. Node.js / Electron / Bun environment
|
|
183
|
+
const isNodeLike = typeof process !== 'undefined' && process.versions && (process.versions.node || process.versions.bun);
|
|
184
|
+
if (isNodeLike) {
|
|
185
|
+
return new NodeFileStorage();
|
|
186
|
+
}
|
|
187
|
+
// 4. Fallback memory
|
|
188
|
+
return new MemoryStorage();
|
|
189
|
+
}
|