antigravity-tc 1.1.3 → 1.1.4

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.id.md CHANGED
@@ -52,9 +52,11 @@ Dashboard terbuka di `http://localhost:3456/dashboard` — tray icon muncul di m
52
52
  | --- | --- |
53
53
  | Halaman donasi publik | Login Google + tempel kode, token dicek eligibility lalu disimpan |
54
54
  | Portal Settings | Atur layout dan kata-kata halaman depan; data disimpan di aplikasi, bukan DB 9Router |
55
- | Multi bahasa | Default English, tersedia Bahasa Indonesia, mudah ditambah bahasa baru |
55
+ | Multi bahasa | UI dashboard dan portal multibahasa (Bahasa Indonesia & English) |
56
56
  | Generate link personal | `?for=Nama` untuk sapaan otomatis |
57
- | Dashboard admin | Statistik, tabel live, search, filter, export/import CSV |
57
+ | Dashboard admin | Statistik, tabel live, search, filter status, sumber, dan tier, export/import CSV |
58
+ | Deteksi & Filter Tier | Menampilkan tier donasi (**Free**, **Plus**, **Pro**, **Ultra**) serta filter cepat di tabel |
59
+ | Export Go Keyring | Salin token donatur format `go-keyring-base64` untuk login paksa ke Antigravity / `antigravity-cli` |
58
60
  | Sync ke 9Router | Upsert semua donation logs ke providerConnections 9Router |
59
61
  | Sync dari 9Router | Ambil providerConnections 9Router ke donation logs lokal |
60
62
  | Check ALL Eligibility | Cek semua row bertoken dengan jeda aman dan rekap status |
@@ -64,6 +66,23 @@ Dashboard terbuka di `http://localhost:3456/dashboard` — tray icon muncul di m
64
66
  | Auto start | Berjalan otomatis mode tray saat OS login |
65
67
  | Keamanan | Login JWT, secret unik per instalasi, tunnel terkunci selama password default |
66
68
 
69
+ ## 🚀 Login ke Official Apps (Antigravity & antigravity-cli)
70
+
71
+ Anda dapat menggunakan token donatur untuk langsung login ke aplikasi resmi **Antigravity** atau **antigravity-cli** tanpa perlu autentikasi ulang via browser:
72
+
73
+ 1. Di tabel donasi admin, klik tombol aksi (**...**) pada baris donatur → pilih **Go Keyring**.
74
+ 2. Salin string `go-keyring-base64:...` atau langsung gunakan perintah terminal yang disediakan:
75
+ - **macOS (Keychain)**:
76
+ ```bash
77
+ security add-generic-password -s gemini -a antigravity -w "<STRING_KEYRING>" -U
78
+ ```
79
+ - **Windows (CMD / PowerShell)**:
80
+ ```powershell
81
+ cmdkey /generic:"gemini:antigravity" /user:"antigravity" /pass:"<STRING_KEYRING>"
82
+ ```
83
+ *(Atau isi manual via **Windows Credential Manager** &rarr; Generic Credentials).*
84
+ 3. Buka Antigravity atau jalankan `antigravity-cli` — sesi login donatur langsung aktif.
85
+
67
86
  ## 📁 Lokasi Data
68
87
 
69
88
  ### Database 9Router
package/README.md CHANGED
@@ -53,9 +53,11 @@ The dashboard opens at `http://localhost:3456/dashboard`, and the tray icon appe
53
53
  | --- | --- |
54
54
  | Public donation portal | Google login + manual code paste flow for donors |
55
55
  | Portal Settings | Customize layout and front-page wording; stored in ATC app config, not 9Router DB |
56
- | Multilingual setup | English by default, Indonesian included, designed for adding more languages later |
56
+ | Multilingual support | Fully localized dashboard and portal in English and Bahasa Indonesia |
57
57
  | Personal donation links | `?for=Name` automatically customizes the greeting |
58
- | Admin dashboard | Stats, live table, search, status/source filters, CSV import/export |
58
+ | Admin dashboard | Stats, live table, search, status/source/tier filters, CSV import/export |
59
+ | Tier Detection & Filter | Displays donor tier (**Free**, **Plus**, **Pro**, **Ultra**) with quick filters |
60
+ | Go Keyring Export | Export donor token as `go-keyring-base64` to force login into official Antigravity apps |
59
61
  | Sync to 9Router | Upsert local donation logs into 9Router `providerConnections` |
60
62
  | Sync from 9Router | Import 9Router `providerConnections` back into local donation logs |
61
63
  | Check ALL Eligibility | Check all token rows with per-row delay and status recap |
@@ -65,6 +67,23 @@ The dashboard opens at `http://localhost:3456/dashboard`, and the tray icon appe
65
67
  | Auto-start | Launches in tray mode when the OS starts |
66
68
  | Security | JWT admin login, per-install secret, tunnel blocked while default password is active |
67
69
 
70
+ ## 🚀 Log in to Official Apps (Antigravity & antigravity-cli)
71
+
72
+ You can use donated tokens to authenticate directly into the official **Antigravity** app or **antigravity-cli** without going through browser OAuth:
73
+
74
+ 1. In the admin donation table, click the action menu (**...**) for any donor &rarr; select **Go Keyring**.
75
+ 2. Copy the `go-keyring-base64:...` string or use the provided one-click terminal commands:
76
+ - **macOS (Keychain)**:
77
+ ```bash
78
+ security add-generic-password -s gemini -a antigravity -w "<KEYRING_STRING>" -U
79
+ ```
80
+ - **Windows (CMD / PowerShell)**:
81
+ ```powershell
82
+ cmdkey /generic:"gemini:antigravity" /user:"antigravity" /pass:"<KEYRING_STRING>"
83
+ ```
84
+ *(Or manually add under **Windows Credential Manager** &rarr; Generic Credentials).*
85
+ 3. Launch Antigravity or execute `antigravity-cli` — the donor session will be active immediately.
86
+
68
87
  ## 📁 Data Locations
69
88
 
70
89
  ### 9Router database
package/bin/restart.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const { execSync, spawn } = require('child_process');
6
+
7
+ const { ensureEnvFile } = require('../src/paths');
8
+ require('dotenv').config({ path: ensureEnvFile(), quiet: true });
9
+ const { getConfig } = require('../src/config');
10
+
11
+ const config = getConfig();
12
+ const port = Number(process.env.PORT || config.get('port') || 3456);
13
+ const LOCK_FILE_PATH = path.join(os.tmpdir(), 'antigravity-token-collector.lock');
14
+
15
+ function isPidAlive(pid) {
16
+ try {
17
+ process.kill(pid, 0);
18
+ return true;
19
+ } catch (e) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ function findRunningPids() {
25
+ const pids = new Set();
26
+
27
+ // 1. Cek via lsof (port listener)
28
+ if (process.platform !== 'win32') {
29
+ try {
30
+ const lsofOut = execSync(`lsof -ti :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
31
+ if (lsofOut) {
32
+ for (const line of lsofOut.split('\n')) {
33
+ const pid = parseInt(line.trim(), 10);
34
+ if (pid && pid !== process.pid) pids.add(pid);
35
+ }
36
+ }
37
+ } catch (e) {}
38
+
39
+ // 2. Cek via ps untuk proses node yang menjalankan index.js
40
+ try {
41
+ const psOut = execSync('ps -eo pid,command', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
42
+ for (const line of psOut.split('\n')) {
43
+ if (line.includes('index.js') && (line.includes('antigravity-tc') || line.includes('antigravity-token-collector'))) {
44
+ const match = line.trim().match(/^(\d+)/);
45
+ if (match) {
46
+ const pid = parseInt(match[1], 10);
47
+ if (pid && pid !== process.pid) pids.add(pid);
48
+ }
49
+ }
50
+ }
51
+ } catch (e) {}
52
+ } else {
53
+ // Windows fallback
54
+ try {
55
+ const netstatOut = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
56
+ for (const line of netstatOut.split('\n')) {
57
+ const parts = line.trim().split(/\s+/);
58
+ const pid = parseInt(parts[parts.length - 1], 10);
59
+ if (pid && pid !== process.pid) pids.add(pid);
60
+ }
61
+ } catch (e) {}
62
+ }
63
+
64
+ return Array.from(pids);
65
+ }
66
+
67
+ async function stopRunningInstances() {
68
+ const pids = findRunningPids();
69
+ if (!pids.length) {
70
+ console.log('ℹ️ Tidak ada instance Antigravity-tc yang sedang aktif.');
71
+ return;
72
+ }
73
+
74
+ console.log(`🔄 Menghentikan instance Antigravity-tc yang sedang berjalan (PID: ${pids.join(', ')})...`);
75
+
76
+ for (const pid of pids) {
77
+ try {
78
+ process.kill(pid, 'SIGTERM');
79
+ } catch (e) {}
80
+ }
81
+
82
+ // Tunggu hingga proses benar-benar berhenti
83
+ const deadline = Date.now() + 2500;
84
+ while (Date.now() < deadline) {
85
+ const alive = pids.filter(isPidAlive);
86
+ if (!alive.length) break;
87
+ await new Promise((resolve) => setTimeout(resolve, 150));
88
+ }
89
+
90
+ // Force kill jika masih hidup
91
+ for (const pid of pids) {
92
+ if (isPidAlive(pid)) {
93
+ try {
94
+ process.kill(pid, 'SIGKILL');
95
+ } catch (e) {}
96
+ }
97
+ }
98
+
99
+ // Bersihkan stale lock file bila ada
100
+ try {
101
+ if (fs.existsSync(LOCK_FILE_PATH)) fs.unlinkSync(LOCK_FILE_PATH);
102
+ const lockDir = `${LOCK_FILE_PATH}.lock`;
103
+ if (fs.existsSync(lockDir)) fs.rmSync(lockDir, { recursive: true, force: true });
104
+ } catch (e) {}
105
+
106
+ console.log('✅ Instance lama berhasil dihentikan.');
107
+ }
108
+
109
+ async function main() {
110
+ await stopRunningInstances();
111
+
112
+ // Beri jeda sejenak agar OS melepaskan socket port sepenuhnya
113
+ await new Promise((resolve) => setTimeout(resolve, 400));
114
+
115
+ console.log('🚀 Memulai ulang Antigravity Token Collector...');
116
+
117
+ const indexPath = path.join(__dirname, '..', 'index.js');
118
+ const args = process.argv.slice(2);
119
+ const child = spawn(process.execPath, [indexPath, ...args], {
120
+ cwd: path.join(__dirname, '..'),
121
+ stdio: 'inherit',
122
+ env: process.env,
123
+ });
124
+
125
+ child.on('exit', (code) => {
126
+ process.exit(code || 0);
127
+ });
128
+ }
129
+
130
+ main().catch((err) => {
131
+ console.error('❌ Gagal merestart aplikasi:', err);
132
+ process.exit(1);
133
+ });
package/build.md CHANGED
@@ -35,6 +35,8 @@ web/dist/
35
35
  npm version major # 1.0.0 → 2.0.0 (breaking change)
36
36
  ```
37
37
 
38
+ > **Tips**: Jika git working tree belum di-commit, gunakan flag `--no-git-tag-version`, misal: `npm version 1.1.4 --no-git-tag-version`.
39
+
38
40
  3. **Lint bersih**: `npm run lint`
39
41
  4. **Verifikasi isi paket** (yang penting — pastikan secret tidak ikut):
40
42
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "antigravity-tc",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Antigravity Token Collector — tray app untuk mengumpulkan & mengelola donasi token Antigravity ke 9Router",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "antigravity-tc": "./index.js",
8
- "antigravity-tc-reset-password": "./bin/reset-password.js"
8
+ "antigravity-tc-reset-password": "./bin/reset-password.js",
9
+ "antigravity-tc-restart": "./bin/restart.js"
9
10
  },
10
11
  "files": [
11
12
  "index.js",
@@ -24,7 +25,8 @@
24
25
  "build:web": "vite build --config web/vite.config.mjs",
25
26
  "lint": "eslint .",
26
27
  "prepublishOnly": "npm run lint && npm run build:web",
27
- "start": "node index.js"
28
+ "start": "node index.js",
29
+ "restart": "node bin/restart.js"
28
30
  },
29
31
  "keywords": [
30
32
  "antigravity",
package/server.js CHANGED
@@ -166,7 +166,9 @@ function initDonationLogDb() {
166
166
  ['eligibility_status', 'TEXT'],
167
167
  ['eligibility_message', 'TEXT'],
168
168
  ['eligibility_details', 'TEXT'],
169
- ['eligibility_checked_at', 'TEXT']
169
+ ['eligibility_checked_at', 'TEXT'],
170
+ ['tier', 'TEXT'],
171
+ ['id_token', 'TEXT']
170
172
  ];
171
173
  for (const [name, type] of additions) {
172
174
  if (!columns.includes(name)) db.exec(`ALTER TABLE donation_logs ADD COLUMN ${name} ${type}`);
@@ -184,6 +186,7 @@ function writeDonationLog({
184
186
  accessToken,
185
187
  refreshToken,
186
188
  expiry,
189
+ idToken,
187
190
  }) {
188
191
  const now = new Date().toISOString();
189
192
  const normalizedEmail = String(donorEmail || '').trim().toLowerCase();
@@ -197,7 +200,9 @@ function writeDonationLog({
197
200
  db.prepare(`
198
201
  UPDATE donation_logs SET
199
202
  donor_name = ?, donor_email = ?, requested_for = ?, source = ?, status = ?, message = ?,
200
- access_token = ?, refresh_token = ?, expiry = ?, created_at = ?, updated_at = ?
203
+ access_token = ?, refresh_token = ?, expiry = ?,
204
+ id_token = COALESCE(?, id_token),
205
+ created_at = ?, updated_at = ?
201
206
  WHERE id = ?
202
207
  `).run(
203
208
  donorName,
@@ -209,6 +214,7 @@ function writeDonationLog({
209
214
  accessToken || '',
210
215
  refreshToken || '',
211
216
  expiry || '',
217
+ idToken || null,
212
218
  now,
213
219
  now,
214
220
  existing.id
@@ -219,8 +225,8 @@ function writeDonationLog({
219
225
  db.prepare(`
220
226
  INSERT INTO donation_logs (
221
227
  id, donor_name, donor_email, requested_for, source, status, message,
222
- access_token, refresh_token, expiry, created_at, updated_at
223
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
228
+ access_token, refresh_token, expiry, id_token, created_at, updated_at
229
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
224
230
  `).run(
225
231
  crypto.randomUUID(),
226
232
  donorName,
@@ -232,6 +238,7 @@ function writeDonationLog({
232
238
  accessToken || '',
233
239
  refreshToken || '',
234
240
  expiry || '',
241
+ idToken || null,
235
242
  now,
236
243
  now
237
244
  );
@@ -271,6 +278,9 @@ function googleRequest(method, url, data, headers = {}) {
271
278
  }
272
279
 
273
280
  function readFromKeychain() {
281
+ if (process.platform !== 'darwin') {
282
+ throw new Error('Fitur ambil langsung dari Keychain hanya didukung di macOS.');
283
+ }
274
284
  const cmd = 'security find-generic-password -s gemini -a antigravity -w ~/Library/Keychains/login.keychain-db';
275
285
  return execSync(cmd, { encoding: 'utf-8' }).trim();
276
286
  }
@@ -363,16 +373,96 @@ function updateDonationLogToken(id, tokenInfo) {
363
373
  }
364
374
  }
365
375
 
376
+ const GOOGLE_TIERS = {
377
+ 'free-tier': 'Free',
378
+ 'g1-plus-tier': 'Plus',
379
+ 'g1-pro-tier': 'Pro',
380
+ 'g1-ultra-tier': 'Ultra'
381
+ };
382
+
383
+ function resolveGoogleTier(paidTier) {
384
+ const raw = paidTier || null;
385
+ if (!raw) return { id: 'free-tier', name: GOOGLE_TIERS['free-tier'] };
386
+ if (typeof raw === 'string') {
387
+ const id = raw.trim().toLowerCase();
388
+ return { id, name: GOOGLE_TIERS[id] || (id.includes('ultra') ? 'Ultra' : id.includes('pro') ? 'Pro' : id.includes('plus') ? 'Plus' : id.includes('free') ? 'Free' : raw) };
389
+ }
390
+ const id = String(raw.id || raw.name || 'free-tier').trim().toLowerCase();
391
+ const text = `${id} ${String(raw.name || '')}`.toLowerCase();
392
+ let name = GOOGLE_TIERS[id];
393
+ if (!name) {
394
+ if (text.includes('ultra')) name = 'Ultra';
395
+ else if (text.includes('pro')) name = 'Pro';
396
+ else if (text.includes('plus')) name = 'Plus';
397
+ else if (text.includes('free')) name = 'Free';
398
+ else name = raw.name || id;
399
+ }
400
+ return { id, name };
401
+ }
402
+
403
+ function parseTier(tierRaw, eligibilityDetailsRaw) {
404
+ if (tierRaw) {
405
+ try {
406
+ const parsed = JSON.parse(tierRaw);
407
+ if (parsed && typeof parsed === 'object') {
408
+ const id = String(parsed.id || '').trim().toLowerCase();
409
+ const text = `${id} ${String(parsed.name || '')}`.toLowerCase();
410
+ let name = GOOGLE_TIERS[id];
411
+ if (!name) {
412
+ if (text.includes('ultra')) name = 'Ultra';
413
+ else if (text.includes('pro')) name = 'Pro';
414
+ else if (text.includes('plus')) name = 'Plus';
415
+ else if (text.includes('free')) name = 'Free';
416
+ else name = parsed.name || id;
417
+ }
418
+ return { id: id || 'custom', name };
419
+ }
420
+ return resolveGoogleTier(tierRaw);
421
+ } catch {
422
+ return resolveGoogleTier(tierRaw);
423
+ }
424
+ }
425
+ if (eligibilityDetailsRaw) {
426
+ try {
427
+ const details = typeof eligibilityDetailsRaw === 'string' ? JSON.parse(eligibilityDetailsRaw) : eligibilityDetailsRaw;
428
+ const pt = details?.paidTier;
429
+ if (pt) return resolveGoogleTier(pt);
430
+ } catch {}
431
+ }
432
+ return null;
433
+ }
434
+
366
435
  // Update status donasi berdasarkan email (key dedupe), dipakai alur 2-fase:
367
436
  // simpan dulu "No Verif" → setelah cek eligibility jadi Success / No Verif / Not Eligible.
368
- function setDonationStatusByEmail(email, status, message) {
437
+ function setDonationStatusByEmail(email, status, message, eligibility) {
369
438
  const now = new Date().toISOString();
370
439
  const normalized = String(email || '').trim().toLowerCase();
371
440
  if (!normalized) return;
372
441
  const db = getLogDb();
373
442
  try {
374
- db.prepare('UPDATE donation_logs SET status = ?, message = ?, updated_at = ? WHERE lower(donor_email) = ?')
375
- .run(status, message || '', now, normalized);
443
+ const tier = eligibility?.paidTier || eligibility?.tier || null;
444
+ const tierJson = tier ? JSON.stringify(tier) : null;
445
+ db.prepare(`
446
+ UPDATE donation_logs SET
447
+ status = ?, message = ?,
448
+ eligibility_status = COALESCE(?, eligibility_status),
449
+ eligibility_message = COALESCE(?, eligibility_message),
450
+ eligibility_details = COALESCE(?, eligibility_details),
451
+ eligibility_checked_at = COALESCE(?, eligibility_checked_at),
452
+ tier = COALESCE(?, tier),
453
+ updated_at = ?
454
+ WHERE lower(donor_email) = ?
455
+ `).run(
456
+ status,
457
+ message || '',
458
+ eligibility?.status || null,
459
+ eligibility?.message || null,
460
+ eligibility?.raw ? JSON.stringify(eligibility.raw) : null,
461
+ eligibility ? now : null,
462
+ tierJson,
463
+ now,
464
+ normalized
465
+ );
376
466
  } finally {
377
467
  db.close();
378
468
  }
@@ -383,10 +473,14 @@ function setDonationStatusById(id, status, message, eligibility) {
383
473
  if (!id) return;
384
474
  const db = getLogDb();
385
475
  try {
476
+ const tier = eligibility?.paidTier || eligibility?.tier || null;
477
+ const tierJson = tier ? JSON.stringify(tier) : null;
386
478
  db.prepare(`
387
479
  UPDATE donation_logs SET
388
480
  status = ?, message = ?, eligibility_status = ?, eligibility_message = ?,
389
- eligibility_details = ?, eligibility_checked_at = ?, updated_at = ?
481
+ eligibility_details = ?, eligibility_checked_at = ?,
482
+ tier = COALESCE(?, tier),
483
+ updated_at = ?
390
484
  WHERE id = ?
391
485
  `).run(
392
486
  status,
@@ -395,6 +489,7 @@ function setDonationStatusById(id, status, message, eligibility) {
395
489
  eligibility?.message || '',
396
490
  JSON.stringify(eligibility?.raw || {}),
397
491
  now,
492
+ tierJson,
398
493
  now,
399
494
  id
400
495
  );
@@ -426,16 +521,17 @@ async function checkAntigravityEligibility(accessToken) {
426
521
  'Content-Type': 'application/json',
427
522
  'User-Agent': userAgent
428
523
  });
429
- const currentTier = response.currentTier || null;
524
+ const paidTier = resolveGoogleTier(response.paidTier);
525
+ const tier = paidTier;
430
526
  const ineligibleTiers = Array.isArray(response.ineligibleTiers) ? response.ineligibleTiers : [];
431
527
  const ineligible = ineligibleTiers[0] || null;
432
528
  const isEligible = ineligibleTiers.length === 0;
433
529
  const needsVerification = ineligible?.reasonCode === 'VALIDATION_REQUIRED';
434
530
  const result = isEligible
435
- ? { eligible: true, status: 'eligible', needVerification: false, currentTier, tier: currentTier, validationUrl: null, message: 'Terimakasih! partisipasi dukungan token Antigravity dari kamu sangat membantu.' }
531
+ ? { eligible: true, status: 'eligible', needVerification: false, paidTier, tier, validationUrl: null, message: 'Terimakasih! partisipasi dukungan token Antigravity dari kamu sangat membantu.' }
436
532
  : needsVerification
437
- ? { eligible: false, status: 'verification_required', needVerification: true, currentTier, tier: currentTier, validationUrl: ineligible.validationUrl || null, message: ineligible.validationErrorMessage || ineligible.reasonMessage || 'Akun Google Anda perlu diverifikasi.' }
438
- : { eligible: false, status: 'failed', needVerification: false, currentTier, tier: currentTier, validationUrl: null, message: ineligible.reasonMessage || response.error?.message || 'Akun tidak eligible untuk Antigravity.' };
533
+ ? { eligible: false, status: 'verification_required', needVerification: true, paidTier, tier, validationUrl: ineligible.validationUrl || null, message: ineligible.validationErrorMessage || ineligible.reasonMessage || 'Akun Google Anda perlu diverifikasi.' }
534
+ : { eligible: false, status: 'failed', needVerification: false, paidTier, tier, validationUrl: null, message: ineligible.reasonMessage || response.error?.message || 'Akun tidak eligible untuk Antigravity.' };
439
535
  // console.log('[eligibility]', JSON.stringify({ endpoint, httpStatus: response._httpStatus || null, status: result.status, reasonCode: ineligible?.reasonCode || null, validationUrlAvailable: Boolean(result.validationUrl) }));
440
536
  return { ...result, raw: response };
441
537
  }
@@ -811,14 +907,15 @@ const server = http.createServer(async (req, res) => {
811
907
  message: 'Menunggu pengecekan eligibility',
812
908
  accessToken: tokenResponse.access_token,
813
909
  refreshToken: tokenResponse.refresh_token,
814
- expiry
910
+ expiry,
911
+ idToken: tokenResponse.id_token || null
815
912
  });
816
913
 
817
914
  // Fase 2: cek eligibility, baru finalkan statusnya.
818
915
  const eligibility = await checkAntigravityEligibility(tokenResponse.access_token);
819
916
  if (eligibility.status !== 'eligible') {
820
917
  const logStatus = eligibility.status === 'verification_required' ? 'No Verif' : 'Not Eligible';
821
- setDonationStatusByEmail(userResponse.email, logStatus, eligibility.message);
918
+ setDonationStatusByEmail(userResponse.email, logStatus, eligibility.message, eligibility);
822
919
  const checkId = eligibility.status === 'verification_required' ? rememberPendingEligibility({
823
920
  name: userResponse.name || userResponse.email,
824
921
  email: userResponse.email,
@@ -835,7 +932,7 @@ const server = http.createServer(async (req, res) => {
835
932
  eligible: false,
836
933
  status: eligibility.status,
837
934
  needVerification: eligibility.needVerification,
838
- currentTier: eligibility.currentTier,
935
+ paidTier: eligibility.paidTier,
839
936
  tier: eligibility.tier,
840
937
  validationUrl: eligibility.validationUrl,
841
938
  verificationUrl: eligibility.validationUrl,
@@ -846,11 +943,12 @@ const server = http.createServer(async (req, res) => {
846
943
  });
847
944
  }
848
945
 
849
- setDonationStatusByEmail(userResponse.email, 'Success', message);
946
+ setDonationStatusByEmail(userResponse.email, 'Success', message, eligibility);
850
947
  return sendJSON(res, 200, {
851
948
  success: true,
852
949
  status: eligibility.status,
853
- currentTier: eligibility.currentTier,
950
+ paidTier: eligibility.paidTier,
951
+ tier: eligibility.tier,
854
952
  token: {
855
953
  access_token: tokenResponse.access_token,
856
954
  token_type: tokenResponse.token_type || 'Bearer',
@@ -907,11 +1005,11 @@ const server = http.createServer(async (req, res) => {
907
1005
  const eligibility = await checkAntigravityEligibility(token.accessToken);
908
1006
  if (eligibility.status !== 'eligible') {
909
1007
  const logStatus = eligibility.status === 'verification_required' ? 'No Verif' : 'Not Eligible';
910
- setDonationStatusByEmail(email, logStatus, eligibility.message);
1008
+ setDonationStatusByEmail(email, logStatus, eligibility.message, eligibility);
911
1009
  return sendJSON(res, 403, { success: false, ...eligibility, details: eligibility.raw, email, checkId: body.checkId });
912
1010
  }
913
1011
  if (pending) pendingEligibilityChecks.delete(String(body.checkId));
914
- setDonationStatusByEmail(email, 'Success', eligibility.message);
1012
+ setDonationStatusByEmail(email, 'Success', eligibility.message, eligibility);
915
1013
  return sendJSON(res, 200, { success: true, message: 'Terimakasih! partisipasi dukungan token Antigravity dari kamu sangat membantu.', ...eligibility });
916
1014
  } catch (error) {
917
1015
  return sendJSON(res, 500, { success: false, error: 'Eligibility recheck gagal: ' + error.message });
@@ -985,7 +1083,7 @@ const server = http.createServer(async (req, res) => {
985
1083
  eligible: result.eligible,
986
1084
  status: result.status,
987
1085
  needVerification: result.needVerification,
988
- currentTier: result.currentTier,
1086
+ paidTier: result.paidTier,
989
1087
  tier: result.tier,
990
1088
  validationUrl: result.validationUrl,
991
1089
  verificationUrl: result.validationUrl,
@@ -1001,7 +1099,15 @@ const server = http.createServer(async (req, res) => {
1001
1099
  try {
1002
1100
  const raw = readFromKeychain();
1003
1101
  const decoded = decodeKeychainData(raw);
1004
- return sendJSON(res, 200, { success: true, token: { accessToken: decoded.token.access_token, refreshToken: decoded.token.refresh_token, expiry: decoded.token.expiry } });
1102
+ return sendJSON(res, 200, {
1103
+ success: true,
1104
+ token: {
1105
+ accessToken: decoded.token.access_token,
1106
+ refreshToken: decoded.token.refresh_token,
1107
+ expiry: decoded.token.expiry,
1108
+ idToken: decoded.id_token || null
1109
+ }
1110
+ });
1005
1111
  } catch (error) { return sendJSON(res, 500, { success: false, error: error.message }); }
1006
1112
  }
1007
1113
 
@@ -1010,7 +1116,15 @@ const server = http.createServer(async (req, res) => {
1010
1116
  try {
1011
1117
  const body = await parseBody(req);
1012
1118
  const decoded = decodeKeychainData(body.raw.trim());
1013
- return sendJSON(res, 200, { success: true, token: { accessToken: decoded.token.access_token, refreshToken: decoded.token.refresh_token, expiry: decoded.token.expiry } });
1119
+ return sendJSON(res, 200, {
1120
+ success: true,
1121
+ token: {
1122
+ accessToken: decoded.token.access_token,
1123
+ refreshToken: decoded.token.refresh_token,
1124
+ expiry: decoded.token.expiry,
1125
+ idToken: decoded.id_token || null
1126
+ }
1127
+ });
1014
1128
  } catch (error) { return sendJSON(res, 500, { success: false, error: 'Gagal decode: ' + error.message }); }
1015
1129
  }
1016
1130
 
@@ -1198,6 +1312,7 @@ const server = http.createServer(async (req, res) => {
1198
1312
  status: result.status,
1199
1313
  logStatus,
1200
1314
  message: result.message,
1315
+ paidTier: result.paidTier,
1201
1316
  tier: result.tier,
1202
1317
  validationUrl: result.validationUrl,
1203
1318
  verificationUrl: result.validationUrl,
@@ -1495,7 +1610,7 @@ const server = http.createServer(async (req, res) => {
1495
1610
  : 'Not Eligible';
1496
1611
  setDonationStatusById(row.id, logStatus, result.message, result);
1497
1612
  recap[logStatus] = (recap[logStatus] || 0) + 1;
1498
- send({ type: 'log', index: index + 1, email: row.donor_email, action: logStatus, status: result.status, message: result.message, tokenRefreshed: token.refreshed });
1613
+ send({ type: 'log', index: index + 1, email: row.donor_email, action: logStatus, status: result.status, tier: result.tier, message: result.message, tokenRefreshed: token.refreshed });
1499
1614
  } catch (error) {
1500
1615
  errors++;
1501
1616
  const message = error.message || 'Eligibility check gagal';
@@ -1522,24 +1637,29 @@ const server = http.createServer(async (req, res) => {
1522
1637
  const db = getLogDb();
1523
1638
  const rows = db.prepare(`
1524
1639
  SELECT donor_name, donor_email, requested_for, source, status,
1525
- created_at, access_token, refresh_token, expiry
1640
+ created_at, access_token, refresh_token, expiry, tier, eligibility_details
1526
1641
  FROM donation_logs
1527
1642
  ORDER BY created_at DESC
1528
1643
  `).all();
1529
1644
  db.close();
1530
1645
 
1531
- const header = ['Name', 'Email', 'RequestedFor', 'Source', 'Status', 'CreatedAt', 'AccessToken', 'RefreshToken', 'Expiry'];
1532
- const csv = [header, ...rows.map((row) => [
1533
- row.donor_name,
1534
- row.donor_email,
1535
- row.requested_for,
1536
- row.source,
1537
- row.status,
1538
- row.created_at,
1539
- row.access_token || '',
1540
- row.refresh_token || '',
1541
- row.expiry || '',
1542
- ])]
1646
+ const header = ['Name', 'Email', 'RequestedFor', 'Source', 'Status', 'Tier', 'CreatedAt', 'AccessToken', 'RefreshToken', 'Expiry'];
1647
+ const csv = [header, ...rows.map((row) => {
1648
+ const tierObj = parseTier(row.tier, row.eligibility_details);
1649
+ const tierName = tierObj ? tierObj.name : '-';
1650
+ return [
1651
+ row.donor_name,
1652
+ row.donor_email,
1653
+ row.requested_for,
1654
+ row.source,
1655
+ row.status,
1656
+ tierName,
1657
+ row.created_at,
1658
+ row.access_token || '',
1659
+ row.refresh_token || '',
1660
+ row.expiry || '',
1661
+ ];
1662
+ })]
1543
1663
  .map((row) => row.map((col) => `"${String(col).replace(/"/g, '""')}"`).join(','))
1544
1664
  .join('\r\n');
1545
1665
 
@@ -1561,6 +1681,7 @@ const server = http.createServer(async (req, res) => {
1561
1681
  const search = String(parsedUrl.searchParams.get('search') || '').trim().toLowerCase();
1562
1682
  const statusFilter = String(parsedUrl.searchParams.get('status') || '').trim();
1563
1683
  const sourceFilter = String(parsedUrl.searchParams.get('source') || '').trim();
1684
+ const tierFilter = String(parsedUrl.searchParams.get('tier') || '').trim().toLowerCase();
1564
1685
  const where = [];
1565
1686
  const params = [];
1566
1687
  if (search) {
@@ -1575,6 +1696,17 @@ const server = http.createServer(async (req, res) => {
1575
1696
  where.push('source = ?');
1576
1697
  params.push(sourceFilter);
1577
1698
  }
1699
+ if (tierFilter) {
1700
+ if (tierFilter === 'ultra') {
1701
+ where.push("(lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%ultra%')");
1702
+ } else if (tierFilter === 'pro') {
1703
+ where.push("(lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%pro%' AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%ultra%')");
1704
+ } else if (tierFilter === 'plus') {
1705
+ where.push("(lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%plus%')");
1706
+ } else if (tierFilter === 'free') {
1707
+ where.push("((lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%free%') OR ((tier IS NOT NULL OR eligibility_details IS NOT NULL) AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%plus%' AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%pro%' AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%ultra%'))");
1708
+ }
1709
+ }
1578
1710
  const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
1579
1711
  const db = getLogDb();
1580
1712
  const total = db.prepare(`SELECT COUNT(*) AS count FROM donation_logs ${whereSql}`).get(...params).count;
@@ -1599,19 +1731,25 @@ const server = http.createServer(async (req, res) => {
1599
1731
  access_token AS accessToken,
1600
1732
  refresh_token AS refreshToken,
1601
1733
  expiry,
1734
+ id_token AS idToken,
1602
1735
  eligibility_status AS eligibilityStatus,
1603
1736
  eligibility_message AS eligibilityMessage,
1604
1737
  eligibility_details AS eligibilityDetails,
1605
1738
  eligibility_checked_at AS eligibilityCheckedAt,
1739
+ tier,
1606
1740
  created_at AS createdAt
1607
1741
  FROM donation_logs
1608
1742
  ${whereSql}
1609
1743
  ORDER BY created_at DESC
1610
1744
  LIMIT ? OFFSET ?
1611
- `).all(...params, pageSize, (page - 1) * pageSize);
1745
+ `).all(...params, pageSize, (page - 1) * pageSize).map((row) => ({
1746
+ ...row,
1747
+ tier: parseTier(row.tier, row.eligibilityDetails)
1748
+ }));
1612
1749
  const filterOptions = {
1613
1750
  statuses: db.prepare("SELECT DISTINCT status AS value FROM donation_logs WHERE status IS NOT NULL AND TRIM(status) != '' ORDER BY status ASC").all().map((row) => row.value),
1614
1751
  sources: db.prepare("SELECT DISTINCT source AS value FROM donation_logs WHERE source IS NOT NULL AND TRIM(source) != '' ORDER BY source ASC").all().map((row) => row.value),
1752
+ tiers: ['Free', 'Plus', 'Pro', 'Ultra'],
1615
1753
  };
1616
1754
  db.close();
1617
1755
  return sendJSON(res, 200, { success: true, donations: rows, page, pageSize, total, pageCount, stats, filterOptions });