@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 ADDED
@@ -0,0 +1,126 @@
1
+ # @tertaut/sdk
2
+
3
+ Official JavaScript/TypeScript SDK for **Tertaut MDaaS** — The universal monetization & licensing engine for modern software (Desktop apps, CLI tools, Chrome Extensions, Web SaaS, and AI Wrappers).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@tertaut/sdk.svg)](https://www.npmjs.com/package/@tertaut/sdk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ---
9
+
10
+ ## 📦 Installation
11
+
12
+ ```bash
13
+ # npm
14
+ npm install @tertaut/sdk
15
+
16
+ # pnpm
17
+ pnpm add @tertaut/sdk
18
+
19
+ # bun
20
+ bun add @tertaut/sdk
21
+
22
+ # yarn
23
+ yarn add @tertaut/sdk
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 🚀 Quick Start
29
+
30
+ ### 1. License Activation & Machine Binding (Desktop / CLI / Electron / Tauri)
31
+ Bind a license key to hardware fingerprint with automatic RS256 offline cryptographic token support:
32
+
33
+ ```typescript
34
+ import { createClient, computeFingerprint } from '@tertaut/sdk'
35
+
36
+ const tertaut = createClient({
37
+ apiUrl: 'https://tertaut.com' // or your self-hosted Tertaut instance
38
+ })
39
+
40
+ // 1. Automatically calculate client hardware fingerprint
41
+ const fingerprint = await computeFingerprint()
42
+
43
+ // 2. Activate license
44
+ const result = await tertaut.license.activate({
45
+ key: 'TRT-XXXX-XXXX-XXXX-XXXX',
46
+ productSlug: 'my-desktop-app',
47
+ activationType: 'MACHINE_BIND',
48
+ deviceFingerprint: fingerprint,
49
+ deviceName: 'MacBook-Pro-M3'
50
+ })
51
+
52
+ if (result.valid) {
53
+ console.log('License activated successfully!')
54
+ console.log('Offline Token (RS256):', result.offlineToken)
55
+ }
56
+ ```
57
+
58
+ ---
59
+
60
+ ### 2. 1-Line License Startup Guard
61
+ Protect your application startup with automatic storage check, online validation, and local RS256 offline fallback:
62
+
63
+ ```typescript
64
+ import { createClient } from '@tertaut/sdk'
65
+
66
+ const tertaut = createClient({ productSlug: 'my-desktop-app' })
67
+
68
+ // Throws an error or exits if license is invalid or revoked
69
+ await tertaut.protect()
70
+ ```
71
+
72
+ ---
73
+
74
+ ### 3. AI API Proxy Shield (Zero Client-Side Secret Leak)
75
+ Call AI providers (OpenAI, Anthropic, Gemini, Groq) using the user's license key without exposing your master API keys to the client:
76
+
77
+ ```typescript
78
+ import { createClient } from '@tertaut/sdk'
79
+
80
+ const tertaut = createClient({
81
+ apiUrl: 'https://tertaut.com',
82
+ licenseKey: 'TRT-XXXX-XXXX-XXXX-XXXX'
83
+ })
84
+
85
+ const response = await tertaut.ai.chat({
86
+ productId: 'prd_my_ai_tool',
87
+ model: 'gpt-4o-mini',
88
+ messages: [{ role: 'user', content: 'Generate a sales summary.' }]
89
+ })
90
+
91
+ console.log(response)
92
+ ```
93
+
94
+ ---
95
+
96
+ ### 4. Webhook Signature Verification (Cloud SaaS & Entitlement)
97
+ Verify incoming webhooks from Tertaut using HMAC-SHA256:
98
+
99
+ ```typescript
100
+ import { webhooks } from '@tertaut/sdk'
101
+
102
+ // In your Express / Next.js / Bun API handler:
103
+ const isValid = await webhooks.verify({
104
+ rawBody: req.rawBody,
105
+ signature: req.headers['x-tertaut-signature'],
106
+ secret: process.env.TERTAUT_WEBHOOK_SECRET!
107
+ })
108
+
109
+ if (!isValid) {
110
+ return res.status(401).send('Invalid signature')
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## ⚙️ Features
117
+ - 🔐 **Universal Licensing**: Machine-binding (hardware hash), Domain-lock, Chrome Extension identity, and deep-link activation.
118
+ - 📴 **Offline Cryptographic Tokens**: RS256 asymmetric signatures verified against Tertaut's public JWKS.
119
+ - 🧠 **AI Proxy Shield**: Vault encryption for LLM keys with real-time token metering.
120
+ - 🔄 **Cloud SaaS Sync**: Webhook helpers for `order.paid`, `subscription.renewed`, and `license.revoked`.
121
+ - 🪶 **Zero Heavy Dependencies**: Works across Node.js (18+), Bun, Deno, Electron, and modern browsers.
122
+
123
+ ---
124
+
125
+ ## 📄 License
126
+ MIT © [PT Retas Lintas Batas](https://tertaut.com)
package/dist/ai.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { AiChatOptions, AiChatResponse, UsageResult } from './types.js';
2
+ export declare class AiClient {
3
+ private request;
4
+ private defaultProductId?;
5
+ constructor(requestFn: <T>(path: string, init?: RequestInit) => Promise<T>, defaultProductId?: string);
6
+ /**
7
+ * Mengirim request chat completion ke model AI via Tertaut AI Proxy Shield.
8
+ * Kunci API builder tetap terenkripsi dan terlindungi di server Tertaut.
9
+ *
10
+ * @example
11
+ * const res = await tertaut.ai.chat({
12
+ * model: 'gpt-4o-mini',
13
+ * messages: [{ role: 'user', content: 'Halo AI!' }]
14
+ * })
15
+ * console.log(res.text)
16
+ */
17
+ chat(opts: AiChatOptions): Promise<AiChatResponse>;
18
+ /**
19
+ * 1 BARIS KODE: Prompt AI cepat dan langsung kembalikan string balasan.
20
+ *
21
+ * @example
22
+ * const summary = await tertaut.ai.complete('Ringkas kode ini: ...')
23
+ */
24
+ complete(prompt: string, model?: string, extraOptions?: Partial<AiChatOptions>): Promise<string>;
25
+ /**
26
+ * Mengambil riwayat dan sisa kuota token pemakaian AI untuk lisensi ini.
27
+ */
28
+ getUsage(productId?: string): Promise<UsageResult>;
29
+ }
package/dist/ai.js ADDED
@@ -0,0 +1,89 @@
1
+ export class AiClient {
2
+ request;
3
+ defaultProductId;
4
+ constructor(requestFn, defaultProductId) {
5
+ this.request = requestFn;
6
+ this.defaultProductId = defaultProductId;
7
+ }
8
+ /**
9
+ * Mengirim request chat completion ke model AI via Tertaut AI Proxy Shield.
10
+ * Kunci API builder tetap terenkripsi dan terlindungi di server Tertaut.
11
+ *
12
+ * @example
13
+ * const res = await tertaut.ai.chat({
14
+ * model: 'gpt-4o-mini',
15
+ * messages: [{ role: 'user', content: 'Halo AI!' }]
16
+ * })
17
+ * console.log(res.text)
18
+ */
19
+ async chat(opts) {
20
+ const productId = opts.productId || this.defaultProductId;
21
+ if (!productId) {
22
+ throw new Error('Wajib menentukan productId untuk memanggil AI Proxy Shield');
23
+ }
24
+ const { productId: _, ...payload } = opts;
25
+ const raw = await this.request(`/api/proxy/${productId}`, {
26
+ method: 'POST',
27
+ body: JSON.stringify(payload),
28
+ });
29
+ // Helper ekstraksi teks universal dari berbagai provider (OpenAI, Anthropic, Gemini)
30
+ let text = '';
31
+ if (raw?.choices?.[0]?.message?.content) {
32
+ text = raw.choices[0].message.content;
33
+ }
34
+ else if (raw?.choices?.[0]?.text) {
35
+ text = raw.choices[0].text;
36
+ }
37
+ else if (Array.isArray(raw?.content) && raw.content[0]?.text) {
38
+ text = raw.content[0].text;
39
+ }
40
+ else if (raw?.candidates?.[0]?.content?.parts?.[0]?.text) {
41
+ text = raw.candidates[0].content.parts[0].text;
42
+ }
43
+ else if (typeof raw?.output === 'string') {
44
+ text = raw.output;
45
+ }
46
+ else if (typeof raw?.message === 'string') {
47
+ text = raw.message;
48
+ }
49
+ else {
50
+ text = typeof raw === 'string' ? raw : JSON.stringify(raw);
51
+ }
52
+ return {
53
+ text,
54
+ raw,
55
+ model: opts.model,
56
+ usage: raw?.usage
57
+ ? {
58
+ promptTokens: raw.usage.prompt_tokens || raw.usage.promptTokens,
59
+ completionTokens: raw.usage.completion_tokens || raw.usage.completionTokens,
60
+ totalTokens: raw.usage.total_tokens || raw.usage.totalTokens,
61
+ }
62
+ : undefined,
63
+ };
64
+ }
65
+ /**
66
+ * 1 BARIS KODE: Prompt AI cepat dan langsung kembalikan string balasan.
67
+ *
68
+ * @example
69
+ * const summary = await tertaut.ai.complete('Ringkas kode ini: ...')
70
+ */
71
+ async complete(prompt, model = 'gpt-4o-mini', extraOptions = {}) {
72
+ const res = await this.chat({
73
+ model,
74
+ messages: [{ role: 'user', content: prompt }],
75
+ ...extraOptions,
76
+ });
77
+ return res.text;
78
+ }
79
+ /**
80
+ * Mengambil riwayat dan sisa kuota token pemakaian AI untuk lisensi ini.
81
+ */
82
+ async getUsage(productId) {
83
+ const pid = productId || this.defaultProductId;
84
+ if (!pid) {
85
+ throw new Error('Wajib menentukan productId untuk mengecek penggunaan AI');
86
+ }
87
+ return this.request(`/api/proxy/${pid}/usage`);
88
+ }
89
+ }
@@ -0,0 +1,22 @@
1
+ type StorageItem = string | number | boolean | null | undefined;
2
+ type StorageValue = StorageItem | StorageValue[] | {
3
+ [key: string]: StorageValue;
4
+ };
5
+ /**
6
+ * CHROME_IDENTITY: id akun Google pembeli via chrome.identity.
7
+ * Tanpa permission identity.getAuthToken, gunakan profileUserInfo.
8
+ */
9
+ export declare function getIdentityId(opts?: {
10
+ interactive?: boolean;
11
+ fallbackToEmail?: boolean;
12
+ }): Promise<string | null>;
13
+ /** Simpan kunci lisensi local di storage.sync (broadcast antar tool milik pembeli). */
14
+ export declare function saveToSyncStorage(items: Record<string, StorageValue>): Promise<boolean>;
15
+ export declare function loadFromSyncStorage<T = StorageValue>(key: string): Promise<T | null>;
16
+ /**
17
+ * Persist journal keyaki (offline lock / aktivasi) ke storage.sync
18
+ * agar verifikasi CHROME_IDENTITY tetap konsisten.
19
+ */
20
+ export declare function persistJournal(licenseKey: string, entry: Record<string, StorageValue>): Promise<boolean>;
21
+ export declare function getExtensionId(): string | null;
22
+ export {};
package/dist/chrome.js ADDED
@@ -0,0 +1,70 @@
1
+ function chromeApi() {
2
+ const raw = globalThis.chrome;
3
+ return raw || undefined;
4
+ }
5
+ const sync = () => chromeApi()?.storage?.sync;
6
+ /**
7
+ * CHROME_IDENTITY: id akun Google pembeli via chrome.identity.
8
+ * Tanpa permission identity.getAuthToken, gunakan profileUserInfo.
9
+ */
10
+ export async function getIdentityId(opts = {}) {
11
+ const identity = chromeApi()?.identity;
12
+ if (!identity)
13
+ return null;
14
+ if (identity.getProfileUserInfo) {
15
+ try {
16
+ const info = await identity.getProfileUserInfo();
17
+ if (info.id)
18
+ return info.id;
19
+ if (info.email)
20
+ return info.email;
21
+ }
22
+ catch {
23
+ /* lanjut ke auth token */
24
+ }
25
+ }
26
+ try {
27
+ const res = await identity.getAuthToken({ interactive: !!opts.interactive });
28
+ const email = await identity.getProfileUserInfo?.();
29
+ return email?.id || res.token;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ /** Simpan kunci lisensi local di storage.sync (broadcast antar tool milik pembeli). */
36
+ export async function saveToSyncStorage(items) {
37
+ const area = sync();
38
+ if (!area)
39
+ return false;
40
+ await area.set(items);
41
+ return true;
42
+ }
43
+ export async function loadFromSyncStorage(key) {
44
+ const area = sync();
45
+ if (!area)
46
+ return null;
47
+ const stored = await area.get(key.split('.')[0]);
48
+ const root = stored[key.split('.')[0]];
49
+ if (typeof root === 'object' && root !== null && key.includes('.')) {
50
+ const path = key.split('.')[1];
51
+ return root[path];
52
+ }
53
+ return root === undefined ? null : root;
54
+ }
55
+ /**
56
+ * Persist journal keyaki (offline lock / aktivasi) ke storage.sync
57
+ * agar verifikasi CHROME_IDENTITY tetap konsisten.
58
+ */
59
+ export async function persistJournal(licenseKey, entry) {
60
+ const area = sync();
61
+ if (!area)
62
+ return false;
63
+ const stored = await area.get(`tertaut_journal_${licenseKey}`);
64
+ const existing = stored[`tertaut_journal_${licenseKey}`] || {};
65
+ await area.set({ [`tertaut_journal_${licenseKey}`]: { ...existing, ...entry } });
66
+ return true;
67
+ }
68
+ export function getExtensionId() {
69
+ return chromeApi()?.runtime?.id ?? chromeApi()?.extension?.getURL?.('') ?? null;
70
+ }
@@ -0,0 +1,124 @@
1
+ import type { SdkOptions, ActivationType, ActivationResult, ValidationResult, OfflineVerifyResult, LicenseCheckResult, ProtectOptions, JwksResponse } from './types.js';
2
+ import { AiClient } from './ai.js';
3
+ import { webhooks } from './webhooks.js';
4
+ import { type LicenseStorage, type StoredLicenseData } from './storage.js';
5
+ export interface ActivateInput {
6
+ key: string;
7
+ productSlug?: string;
8
+ activationType?: ActivationType;
9
+ deviceFingerprint?: string;
10
+ deviceName?: string;
11
+ domain?: string;
12
+ chromeIdentityId?: string;
13
+ }
14
+ export interface ValidateInput {
15
+ key?: string;
16
+ productSlug?: string;
17
+ deviceFingerprint?: string;
18
+ domain?: string;
19
+ chromeIdentityId?: string;
20
+ offlineToken?: string;
21
+ }
22
+ export interface DeactivateInput {
23
+ key: string;
24
+ deviceFingerprint?: string;
25
+ domain?: string;
26
+ }
27
+ export interface ChatInput {
28
+ productId: string;
29
+ provider?: string;
30
+ model: string;
31
+ messages?: unknown[];
32
+ prompt?: string;
33
+ stream?: boolean;
34
+ [key: string]: unknown;
35
+ }
36
+ export interface TertautClient {
37
+ /** Endpoint universal ping. */
38
+ health(): Promise<{
39
+ status: string;
40
+ uptime?: number;
41
+ version?: string;
42
+ }>;
43
+ link(): string;
44
+ /**
45
+ * 1 BARIS KODE: Proteksi lisensi software otomatis.
46
+ * Cek storage -> validasi online -> fallback RS256 offline token.
47
+ */
48
+ guard(options?: ProtectOptions): Promise<LicenseCheckResult>;
49
+ /**
50
+ * Proteksi dengan melempar error jika lisensi tidak sah.
51
+ * Sangat ideal untuk startup guard atau middleware.
52
+ */
53
+ protect(options?: ProtectOptions): Promise<LicenseCheckResult>;
54
+ /**
55
+ * 1 BARIS KODE: Aktivasi lisensi dan langsung simpan ke storage perangkat.
56
+ */
57
+ activate(key: string, input?: Partial<ActivateInput>): Promise<ActivationResult>;
58
+ /**
59
+ * Melepas lisensi dari perangkat ini dan membersihkan cache.
60
+ */
61
+ deactivate(key?: string): Promise<{
62
+ deactivated: boolean;
63
+ }>;
64
+ /**
65
+ * Cek cepat apakah perangkat sudah memiliki lisensi tersimpan.
66
+ */
67
+ isActivated(): Promise<boolean>;
68
+ /**
69
+ * Mengambil data lisensi yang tersimpan di storage lokal.
70
+ */
71
+ getStored(): Promise<StoredLicenseData | null>;
72
+ /** Storage adapter yang sedang aktif. */
73
+ storage: LicenseStorage;
74
+ /** Low-level licensing engine endpoints. */
75
+ license: {
76
+ activate(input: ActivateInput): Promise<ActivationResult>;
77
+ validate(input: ValidateInput): Promise<ValidationResult>;
78
+ deactivate(input: DeactivateInput): Promise<{
79
+ deactivated: boolean;
80
+ devicesRemaining: number;
81
+ }>;
82
+ verifyOfflineToken(token: string): Promise<OfflineVerifyResult>;
83
+ jwks(): Promise<JwksResponse>;
84
+ };
85
+ /** Client AI Proxy Shield (OpenAI, Anthropic, Gemini, OpenRouter). */
86
+ ai: AiClient;
87
+ /** Helper verifikasi webhook masuk dari platform Tertaut. */
88
+ webhooks: typeof webhooks;
89
+ /** Fake Door Test & Lead Capture (waitlist/early-bird form eksternal). */
90
+ fakeDoorTest: {
91
+ get(slug?: string): Promise<any>;
92
+ recordView(slug?: string): Promise<{
93
+ success: boolean;
94
+ }>;
95
+ submitLead(lead: {
96
+ email: string;
97
+ name?: string;
98
+ slug?: string;
99
+ }): Promise<{
100
+ success: boolean;
101
+ message?: string;
102
+ }>;
103
+ };
104
+ fakeDoor: {
105
+ get(slug?: string): Promise<any>;
106
+ recordView(slug?: string): Promise<{
107
+ success: boolean;
108
+ }>;
109
+ submitLead(lead: {
110
+ email: string;
111
+ name?: string;
112
+ slug?: string;
113
+ }): Promise<{
114
+ success: boolean;
115
+ message?: string;
116
+ }>;
117
+ };
118
+ /** Informasi produk publik, versi, & changelog. */
119
+ products: {
120
+ get(slug?: string): Promise<any>;
121
+ versions(slug?: string): Promise<any[]>;
122
+ };
123
+ }
124
+ export declare function createClient(opts?: SdkOptions): TertautClient;
package/dist/client.js ADDED
@@ -0,0 +1,124 @@
1
+ import { LicenseGuard } from './guard.js';
2
+ import { AiClient } from './ai.js';
3
+ import { webhooks } from './webhooks.js';
4
+ import { createUniversalStorage } from './storage.js';
5
+ function buildHeaders(opts) {
6
+ const headers = { 'Content-Type': 'application/json' };
7
+ if (opts.accessToken)
8
+ headers['Authorization'] = `Bearer ${opts.accessToken}`;
9
+ if (opts.licenseKey)
10
+ headers['X-License-Key'] = opts.licenseKey;
11
+ return headers;
12
+ }
13
+ export function createClient(opts = {}) {
14
+ const envUrl = typeof process !== 'undefined' ? process.env?.TERTAUT_API_URL : undefined;
15
+ const rawUrl = opts.apiUrl || envUrl || 'https://tertaut.com';
16
+ // Menangani input root domain ("https://tertaut.com") maupun URL api ("https://tertaut.com/api")
17
+ const base = rawUrl.replace(/\/+$/, '').replace(/\/api$/, '');
18
+ const headers = buildHeaders(opts);
19
+ async function request(path, init = {}) {
20
+ const res = await fetch(`${base}${path}`, {
21
+ ...init,
22
+ headers: { ...headers, ...init.headers },
23
+ });
24
+ if (!res.ok) {
25
+ let message = `Tertaut API ${path} → HTTP ${res.status}`;
26
+ try {
27
+ const body = await res.clone().json();
28
+ if (body && (body.message || body.error))
29
+ message = body.message || body.error;
30
+ }
31
+ catch {
32
+ /* ignore */
33
+ }
34
+ throw new Error(message);
35
+ }
36
+ const body = (await res.json());
37
+ let data = body;
38
+ while (data && typeof data === 'object' && !Array.isArray(data) && 'data' in data) {
39
+ data = data.data;
40
+ }
41
+ return data;
42
+ }
43
+ const storage = opts.storage || createUniversalStorage();
44
+ const license = {
45
+ activate(input) {
46
+ return request('/api/licenses/activate', {
47
+ method: 'POST',
48
+ body: JSON.stringify(input),
49
+ });
50
+ },
51
+ validate(input) {
52
+ return request('/api/licenses/validate', {
53
+ method: 'POST',
54
+ body: JSON.stringify(input),
55
+ });
56
+ },
57
+ deactivate(input) {
58
+ return request('/api/licenses/deactivate', {
59
+ method: 'POST',
60
+ body: JSON.stringify(input),
61
+ });
62
+ },
63
+ verifyOfflineToken(token) {
64
+ return request('/api/licenses/verify-offline-token', {
65
+ method: 'POST',
66
+ body: JSON.stringify({ token }),
67
+ });
68
+ },
69
+ jwks() {
70
+ return request('/api/licenses/.well-known/jwks');
71
+ },
72
+ };
73
+ const ai = new AiClient(request, opts.productId);
74
+ const rawClient = {
75
+ health() {
76
+ return request('/health');
77
+ },
78
+ link() {
79
+ return base;
80
+ },
81
+ storage,
82
+ license,
83
+ ai,
84
+ webhooks,
85
+ fakeDoorTest: {
86
+ get(slug) {
87
+ const s = slug || opts.productSlug;
88
+ return request(`/api/fakedoortests/public/${encodeURIComponent(s || '')}`);
89
+ },
90
+ recordView(slug) {
91
+ const s = slug || opts.productSlug;
92
+ return request(`/api/fakedoortests/public/${encodeURIComponent(s || '')}/view`, { method: 'POST' });
93
+ },
94
+ submitLead(lead) {
95
+ const s = lead.slug || opts.productSlug;
96
+ return request(`/api/fakedoortests/public/${encodeURIComponent(s || '')}/lead`, {
97
+ method: 'POST',
98
+ body: JSON.stringify({ email: lead.email, name: lead.name }),
99
+ });
100
+ },
101
+ },
102
+ get fakeDoor() {
103
+ return this.fakeDoorTest;
104
+ },
105
+ products: {
106
+ get(slug) {
107
+ const s = slug || opts.productSlug;
108
+ return request(`/api/products/${encodeURIComponent(s || '')}`);
109
+ },
110
+ versions(slug) {
111
+ const s = slug || opts.productSlug;
112
+ return request(`/api/products/${encodeURIComponent(s || '')}/versions`);
113
+ },
114
+ },
115
+ guard: (options) => guardInstance.guard(options),
116
+ protect: (options) => guardInstance.protect(options),
117
+ activate: (key, input) => guardInstance.activate(key, input),
118
+ deactivate: (key) => guardInstance.deactivate(key),
119
+ isActivated: () => guardInstance.isActivated(),
120
+ getStored: () => guardInstance.getStored(),
121
+ };
122
+ const guardInstance = new LicenseGuard(rawClient, { ...opts, storage });
123
+ return rawClient;
124
+ }
@@ -0,0 +1,25 @@
1
+ import type { JwksResponse, OfflineVerifyResult } from './types.js';
2
+ /**
3
+ * Verifikasi offline RS256 penuh (WebCrypto, tanpa jaringan).
4
+ * JWKS di-cache sampai keluar area konteks/versi.
5
+ */
6
+ export declare function verifyOfflineLocally(token: string, source?: JwksResponse | (() => Promise<JwksResponse>)): Promise<OfflineVerifyResult>;
7
+ /**
8
+ * Fingerprint stable berbasis env: platform, arsitektur, concurrency, memori,
9
+ * userAgent (browser/desktop) — di-hash SHA-256. MACHINE_BIND.
10
+ */
11
+ export declare function computeFingerprint(extraSeed?: string): Promise<string>;
12
+ /**
13
+ * Deep link Android (intent://... atau Schema URI) untuk auto-fill lisensi.
14
+ * Returns URI scheme + juga URI action untuk fallback.
15
+ */
16
+ export declare function buildDeepLink(opts: {
17
+ key: string;
18
+ productSlug: string;
19
+ appType?: string;
20
+ apiUrl: string;
21
+ fallbackUrl?: string;
22
+ }): {
23
+ uri: string;
24
+ intentUrl: string;
25
+ };
@@ -0,0 +1,57 @@
1
+ import { verifyOfflineToken } from './jwt.js';
2
+ let cachedJwks = null;
3
+ /**
4
+ * Verifikasi offline RS256 penuh (WebCrypto, tanpa jaringan).
5
+ * JWKS di-cache sampai keluar area konteks/versi.
6
+ */
7
+ export async function verifyOfflineLocally(token, source = () => Promise.reject(new Error('Wajib berikan JWKS: ambil dari /api/licenses/.well-known/jwks saat masih online'))) {
8
+ let jwks = source;
9
+ if (typeof source !== 'function' && source && Array.isArray(source.keys)) {
10
+ cachedJwks = source;
11
+ jwks = source;
12
+ }
13
+ const resolved = typeof jwks === 'function'
14
+ ? cachedJwks ?? (await jwks())
15
+ : jwks;
16
+ return verifyOfflineToken(token, resolved);
17
+ }
18
+ const encoder = new TextEncoder();
19
+ async function sha256Hex(input) {
20
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', encoder.encode(input));
21
+ return Array.from(new Uint8Array(digest))
22
+ .map((b) => b.toString(16).padStart(2, '0'))
23
+ .join('');
24
+ }
25
+ /**
26
+ * Fingerprint stable berbasis env: platform, arsitektur, concurrency, memori,
27
+ * userAgent (browser/desktop) — di-hash SHA-256. MACHINE_BIND.
28
+ */
29
+ export async function computeFingerprint(extraSeed = '') {
30
+ const processLike = globalThis.process;
31
+ const nav = typeof navigator !== 'undefined'
32
+ ? { ua: navigator.userAgent, platform: navigator.platform, cores: navigator.hardwareConcurrency }
33
+ : { ua: processLike?.platform || 'node', platform: '', cores: 0 };
34
+ const deviceMemory = nav.deviceMemory || '';
35
+ const seed = [
36
+ nav.platform,
37
+ processLike?.arch || '',
38
+ nav.cores || 0,
39
+ deviceMemory,
40
+ nav.ua,
41
+ extraSeed,
42
+ ].join('|');
43
+ return sha256Hex(seed);
44
+ }
45
+ /**
46
+ * Deep link Android (intent://... atau Schema URI) untuk auto-fill lisensi.
47
+ * Returns URI scheme + juga URI action untuk fallback.
48
+ */
49
+ export function buildDeepLink(opts) {
50
+ const params = new URLSearchParams({ key: opts.key, product: opts.productSlug, api: opts.apiUrl });
51
+ if (opts.appType)
52
+ params.set('appType', opts.appType);
53
+ const scheme = `tertaut://activate?${params.toString()}`;
54
+ const fallback = opts.fallbackUrl || `${opts.apiUrl}/p/${encodeURIComponent(opts.productSlug)}?key=${opts.key}`;
55
+ const intentUrl = `intent://activate?${params.toString()}#Intent;scheme=tertaut;package=com.tertaut.activation;S.browser_fallback_url=${encodeURIComponent(fallback)};end`;
56
+ return { uri: scheme, intentUrl };
57
+ }