xoichim 1.0.1__tar.gz

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.
xoichim-1.0.1/PKG-INFO ADDED
@@ -0,0 +1,1224 @@
1
+ Metadata-Version: 2.4
2
+ Name: xoichim
3
+ Version: 1.0.1
4
+ Summary: Trần Đình Hoàng
5
+ Author: Hoang
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://app.hdxmax.vn/
8
+ Project-URL: YouTube, https://www.youtube.com/@chachanhgiatay02
9
+ Keywords: gecmax,python,library
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: cryptography>=41
22
+ Provides-Extra: curl
23
+ Requires-Dist: curl_cffi>=0.9.0; extra == "curl"
24
+ Provides-Extra: dev
25
+ Requires-Dist: build; extra == "dev"
26
+ Requires-Dist: twine; extra == "dev"
27
+ Requires-Dist: curl_cffi>=0.9.0; extra == "dev"
28
+
29
+ # golike-gauth
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/golike-gauth.svg)](https://pypi.org/project/golike-gauth/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/golike-gauth.svg)](https://pypi.org/project/golike-gauth/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+
35
+ Client auth for the **Golike gateway API** (2026): JWT + device headers, TikTok `sig` minting, optional legacy `g-auth`.
36
+
37
+ **repup lại:** https://www.youtube.com/@chachanhgiatay02
38
+ **Version:** `1.1.3
39
+ ## Changelog
40
+
41
+ ### 1.1.3 — Golike 3.0 path map - Tran Dinh Hoang
42
+ - Facebook `complete-jobs-2026` → complete_job
43
+ - Traffic / review-jobs sig map
44
+ - Generic publishers/<plt>/jobs (bluesky, lazada, pinterest, ...)
45
+ - Crypto/captcha không đổi
46
+
47
+ ### 0.1.14 — Facebook complete-jobs-2026 + multi-platform sig
48
+
49
+ - Map `complete-jobs-2026` → `('facebook', 'complete_job')`
50
+ - Map `get-jobs-2026` → `('facebook', 'get_job')`
51
+ - Sig tự động cho mọi `publishers/<plt>/jobs` và `complete-jobs`
52
+ (TikTok, Instagram, Threads, Twitter, YouTube, Facebook, …)
53
+ - `auto_solve_captcha` giữ nguyên
54
+
55
+ ### 0.1.13 — Sig + captcha cho mọi platform job
56
+
57
+ - Mọi endpoint lấy job publisher giờ đều cần header `sig` (và có thể bắt captcha):
58
+ - TikTok, Facebook, Instagram, Threads, Twitter/X, YouTube, …
59
+ - `resolve_sig_act(path)` map tự động theo pattern:
60
+
61
+ | Path | plt | act |
62
+ |------|-----|-----|
63
+ | `/publishers/<plt>/jobs` | `<plt>` | `get_job` |
64
+ | `/publishers/<plt>/complete-jobs` | `<plt>` | `complete_job` |
65
+ | `/publishers/get-jobs-2026` | `facebook` | `get_job` |
66
+ | `*/skip-jobs` | — | không mint sig |
67
+
68
+ - `enable_sig=True` + `captcha_solver=auto_solve_captcha` →
69
+ `auth.get(...)` tự mint sig, tự giải captcha, không cần gọi
70
+ `mint_security_token` thủ công.
71
+ - `resolve_tiktok_sig_act` giữ nguyên (alias tương thích ngược).
72
+ - Không breaking change với code chỉ dùng TikTok.
73
+
74
+ #### Cách dùng
75
+
76
+ ```python
77
+ from xoichim import GolikeAuth, auto_solve_captcha
78
+
79
+ auth = GolikeAuth.from_token(
80
+ "eyJ...",
81
+ enable_sig=True,
82
+ fetch_session=True,
83
+ captcha_solver=auto_solve_captcha,
84
+ captcha_max_attempts=3,
85
+ )
86
+
87
+ # Instagram
88
+ auth.get("/advertising/publishers/instagram/jobs",
89
+ params={"instagram_account_id": "978936", "data": "null"})
90
+
91
+ # Threads
92
+ auth.get("/advertising/publishers/threads/jobs")
93
+
94
+ # Twitter / X
95
+ auth.get("/advertising/publishers/twitter/jobs",
96
+ params={"account_id": "132488"})
97
+
98
+ # YouTube
99
+ auth.get("/advertising/publishers/youtube/jobs",
100
+ params={"account_id": "61420"})
101
+
102
+ # Facebook
103
+ auth.get("/advertising/publishers/get-jobs-2026",
104
+ params={"fb_id": "61582501696566", "server": "sv2"})
105
+
106
+ # TikTok
107
+ auth.get("/advertising/publishers/tiktok/jobs",
108
+ params={"account_id": "708615", "data": "null"})
109
+
110
+ ### 0.1.12 — Bundle `26.08.01.1` / `gweb-v2` + interactive challenge
111
+ print(r.status_code, r.json())
112
+ ```
113
+
114
+ #### Kiểm tra map path
115
+
116
+ ```python
117
+ from xoichim import resolve_sig_act
118
+
119
+ print(resolve_sig_act("/advertising/publishers/tiktok/jobs"))
120
+ # → ('tiktok', 'get_job')
121
+
122
+ print(resolve_sig_act("/advertising/publishers/get-jobs-2026"))
123
+ # → ('facebook', 'get_job')
124
+ ```
125
+
126
+ #### Lưu ý quan trọng
127
+
128
+ 1. **Phải truyền `captcha_solver`** nếu không muốn bị
129
+ `SecurityCaptchaRequired`:
130
+
131
+ ```python
132
+ auth = GolikeAuth.from_token(
133
+ TOKEN,
134
+ enable_sig=True,
135
+ captcha_solver=auto_solve_captcha, # bắt buộc nếu hay bị captcha
136
+ )
137
+ ```
138
+
139
+ 2. Path gateway **không** có prefix `/api` thừa:
140
+
141
+ ```python
142
+ # Đúng
143
+ auth.get("/advertising/publishers/get-jobs-2026", params={...})
144
+
145
+ # Sai → 404 (base URL đã có /api)
146
+ auth.get("/api/advertising/publishers/get-jobs-2026", params={...})
147
+ ```
148
+
149
+ 3. `sig` **không reuse** được lâu / sang query khác.
150
+ Mỗi lần `auth.get(...)` thư viện mint `sig` mới gắn đúng path + query.
151
+
152
+ 4. `enable_sig=False` (mặc định) → không mint sig cho bất kỳ platform nào
153
+ (giữ hành vi an toàn cho API không cần sig).
154
+
155
+ #### Breaking changes
156
+
157
+ Không có. API công khai cũ vẫn hoạt động:
158
+
159
+ - `resolve_tiktok_sig_act(path)` → vẫn trả `act` string hoặc `None`
160
+ - `GolikeAuth.from_token(..., enable_sig=True)` → TikTok như trước
161
+ - `mint_security_token(..., plt=..., act=...)` → vẫn nhận tham số thủ công
162
+ auth = GolikeAuth.from_token(
163
+ "eyJ...",
164
+ enable_sig=True,
165
+ fetch_session=True,
166
+ captcha_solver=auto_solve_captcha, # tự động giải
167
+ captcha_max_attempts=3,
168
+ )
169
+
170
+ r = auth.get(
171
+ "/advertising/publishers/tiktok/jobs",
172
+ params={"account_id": "708615", "data": "null"},
173
+ )
174
+ print(r.status_code, r.json())
175
+ ```
176
+
177
+ Hoặc giải captcha riêng:
178
+
179
+ ```python
180
+ from gecmax import solve_security_captcha, auto_solve_captcha
181
+
182
+ ok = solve_security_captcha(
183
+ solver=auto_solve_captcha,
184
+ signing_key=auth.signing_key,
185
+ device_id=auth.device_id,
186
+ user_id=auth.user_id,
187
+ token=auth.token,
188
+ max_attempts=3,
189
+ )
190
+ print("captcha solved:", ok)
191
+ ```
192
+
193
+ #### Thuật toán path (tóm tắt)
194
+
195
+ 1. Đoạn thẳng từ `start` → **tâm** `waypoint` (~45 điểm).
196
+ 2. Đoạn thẳng từ tâm waypoint → **tâm** `target` (~50 điểm).
197
+ 3. Giữ yên tại tâm target (5 điểm ổn định).
198
+ 4. Timestamp tăng dần (`elapsed_ms`).
199
+
200
+ Vì mọi điểm đều đi qua tâm vòng tròn, tỷ lệ chấp nhận hình học thường
201
+ đạt **90–98%** khi server chỉ check geometry. Nên giữ
202
+ `captcha_max_attempts=3` để xử lý các lần fail hiếm.
203
+
204
+ #### Lưu ý
205
+
206
+ - `auto_solve_captcha` là **tùy chọn**. Vẫn có thể truyền callback UI
207
+ người dùng (như 0.1.11) nếu server bắt đầu chấm điểm giống người.
208
+ - Path job TikTok phải là dạng relative của gateway
209
+ (`/advertising/publishers/tiktok/jobs`), **không** có `/api` ở đầu
210
+ (base URL đã kết thúc bằng `/api`).
211
+ - `resolve_tiktok_sig_act` giờ cũng nhận diện
212
+ `publishers/tiktok/jobs` ngoài dạng cũ `/tiktok/jobs`.
213
+
214
+ - `/api/v1/security/session` sends `g-scheme: gweb-v2`.
215
+ - The pure-Python signer now matches the bundle's WASM `sign_v2` output.
216
+ - A session response may still report `schemeVersion: "gweb-v1"`; that field is
217
+ metadata and does not make bundle `26.08.01.1` downgrade its selected signer.
218
+ - If `/api/v1/security/token` returns `action/reason: "captcha"`, the client can
219
+ fetch `/security/captcha/challenge`, submit human pointer samples to
220
+ `/security/captcha/verify`, then retry the original token mint with the same
221
+ signing key.
222
+ - Session re-handshake remains separate: it happens only for a retryable mint
223
+ failure, not after a successful CAPTCHA.
224
+
225
+ The library does not automate or bypass the drag challenge. Supply a callback
226
+ that presents `challenge["geometry"]` to a user and returns the collected
227
+ `[elapsed_ms, x, y]` samples:
228
+
229
+ ```python
230
+ from xoichim import GolikeAuth
231
+
232
+ def collect_drag_points(challenge, message):
233
+ # Render the 300x200 logical geometry in your UI and collect real input.
234
+ # Return None if the user cancels.
235
+ return my_ui.collect_pointer_path(challenge["geometry"], message)
236
+
237
+ auth = GolikeAuth.from_token(
238
+ "eyJ...",
239
+ enable_sig=True,
240
+ captcha_solver=collect_drag_points,
241
+ )
242
+ ```
243
+
244
+ ### 0.1.10 — Bundle `26.07.27.4` / `gweb-v1` TikTok signer
245
+
246
+ This supersedes the older sectoken scheme documented below:
247
+
248
+ - `/api/v1/security/session` now sends `g-scheme: gweb-v1`.
249
+ - `/api/v1/security/token` uses the bundle's `gweb-v1` signer (ported from
250
+ the embedded WASM to pure Python).
251
+ - The client keeps the optional gateway `firebase_id` key separate from the
252
+ security-session key used to mint TikTok `sig`.
253
+ - If token minting returns no token, the client refreshes the security session
254
+ and retries once, matching the web app.
255
+ - `mint_security_token(..., telemetry={...})` and
256
+ `request(..., sig_telemetry={...})` can include the bundle's optional `tel`
257
+ object.
258
+
259
+ To run the source checkout rather than an older wheel in `site-packages`:
260
+
261
+ ```powershell
262
+ python -m pip install -e ".[curl]"
263
+ $env:GOLIKE_TOKEN="eyJ..."
264
+ python .\examples\z.py
265
+ ```
266
+
267
+ ### 0.1.9 — `enable_sig` default `False`
268
+
269
+ `sig` chỉ dùng cho TikTok jobs/complete. Mặc định **tắt** để không ảnh hưởng API job nền tảng khác (Facebook, Instagram, …).
270
+
271
+ ```python
272
+ # Default — no sig on any request
273
+ auth = GolikeAuth.from_token("eyJ...")
274
+
275
+ # Facebook / multi jobs — no sig
276
+ auth.get("/advertising/publishers/get-jobs-2026", params={...})
277
+
278
+ # TikTok — must enable explicitly
279
+ auth = GolikeAuth.from_token("eyJ...", enable_sig=True)
280
+ auth.get(
281
+ "/advertising/publishers/tiktok/jobs",
282
+ params={"account_id": "711964", "data": "null"},
283
+ )
284
+
285
+ # Or per-request override
286
+ auth.get(".../tiktok/jobs", params={...}, with_sig=True)
287
+ ```
288
+
289
+ | `enable_sig` | Behavior |
290
+ |---|---|
291
+ | `False` (default) | Never mint/send `sig` |
292
+ | `True` | Mint `sig` only for paths matching `/tiktok/jobs` or `/tiktok/complete-jobs` |
293
+
294
+ ### 0.1.8 — TikTok `sig` encryption overhaul (JS bundle 24/7)
295
+
296
+ > Historical reference only. The sectoken `v3.1` algorithm in this section is
297
+ > not accepted by the current bundle; use the `gweb-v2` flow in 0.1.11 above.
298
+
299
+ App no longer puts a locally encrypted blob directly into `sig`.
300
+ TikTok jobs use a **two-step security API** on `api.golike.net`, with a **new AES-GCM scheme** separate from gateway `g-auth`.
301
+
302
+ #### Request flow
303
+
304
+ ```text
305
+ JWT
306
+ │
307
+ ├─① POST https://api.golike.net/api/v1/security/session
308
+ │ body: {}
309
+ │ → { signing_key, exp, epoch, schemeVersion: "v3.2" }
310
+ │
311
+ ├─② POST https://api.golike.net/api/v1/security/token
312
+ │ body: { plt, act, req: { method, path, query, body } }
313
+ │ headers: Authorization, g-auth (sectoken scheme), g-device-id
314
+ │ → { token } ← this value is the gateway header `sig`
315
+ │
316
+ └─③ GET https://gateway.golike.net/api/advertising/publishers/tiktok/jobs?...
317
+ headers: Authorization, g-device-id, g-username, t, sig
318
+ (no g-auth on the gateway TikTok call)
319
+ ```
320
+
321
+ Missing `sig` on TikTok jobs → HTTP **403**
322
+ `Vui lòng tải lại trang để cập nhật phiên bản mới nhất.`
323
+
324
+ #### Two crypto schemes
325
+
326
+ | | Gateway `g-auth` (legacy / optional) | Sectoken mint (`security/token`) |
327
+ |---|---|---|
328
+ | Used for | Optional gateway binding | Build `g-auth` **only** for step ② |
329
+ | HKDF salt | `glk-gauth-v3-2026q3` | `glk-sectoken-v31-2026q3` |
330
+ | HKDF info | `aes-gcm-key` | `aes-gcm-key-v31` |
331
+ | Digest `r` | `sha256(t:deviceId:bodyHash:salt)[:16]` | `sha256(salt:t:userId:bodyHash)[:16]` |
332
+ | Extra field | — | `r2 = sha256hex(deviceId\|METHOD\|path\|salt)[16:40]` |
333
+ | Output header | `g-auth` | server returns `token` → client sends as **`sig`** |
334
+
335
+ Cipher for both: **AES-256-GCM**, output `base64url(iv12 ‖ ciphertext+tag)`.
336
+
337
+ Sectoken payload example (step ② `g-auth` plaintext):
338
+
339
+ ```json
340
+ {
341
+ "t": 1784884100123,
342
+ "x": "<nonce b64url>",
343
+ "d": "<device-uuid>",
344
+ "u": 639111,
345
+ "n": "POST",
346
+ "k": "/api/v1/security/token",
347
+ "q": "<sha256 hex of mint body>",
348
+ "r": "<16 hex chars>",
349
+ "r2": "<24 hex chars>"
350
+ }
351
+ ```
352
+
353
+ Mint body (`req.path` is gateway pathname, including `/api/...`):
354
+
355
+ ```json
356
+ {
357
+ "plt": "tiktok",
358
+ "act": "get_job",
359
+ "req": {
360
+ "method": "GET",
361
+ "path": "/api/advertising/publishers/tiktok/jobs",
362
+ "query": "account_id=711964&data=null",
363
+ "body": ""
364
+ }
365
+ }
366
+ ```
367
+
368
+ `act` mapping:
369
+
370
+ | Gateway path contains | `act` |
371
+ |---|---|
372
+ | `/tiktok/jobs` (not skip) | `get_job` |
373
+ | `/tiktok/complete-jobs` | `complete_job` |
374
+
375
+ #### Library API (0.1.8)
376
+
377
+ ```python
378
+ from xoichim import GolikeAuth
379
+
380
+ auth = GolikeAuth.from_token("eyJ...", enable_sig=True) # TikTok needs sig
381
+ # 1) GET gateway /users/me
382
+ # 2) POST security/session → signing_key
383
+ # 3) on TikTok get/post: mint security/token → header sig
384
+
385
+ r = auth.get(
386
+ "/advertising/publishers/tiktok/jobs",
387
+ params={"account_id": "711964", "data": "null"},
388
+ )
389
+ print(r.status_code, r.json())
390
+ ```
391
+
392
+ Public helpers:
393
+
394
+ | Function | Role |
395
+ |---|---|
396
+ | `fetch_security_session(token)` | step ① |
397
+ | `mint_security_token(...)` / `generate_sig(...)` | step ② |
398
+ | `generate_sectoken_g_auth(...)` | local AES for step ② `g-auth` |
399
+ | `generate_g_auth(...)` | legacy gateway scheme |
400
+ | `auth.refresh_signing_key()` | refresh before `exp` |
401
+
402
+ Constants: `SALT`, `SECTOKEN_SALT`, `SECURITY_API`, `APP_VERSION` (`26.07.24.1`).
403
+
404
+ #### Breaking notes vs 0.1.6 / early 0.1.7
405
+
406
+ - Do **not** put gateway-style `generate_g_auth(...)` into header `sig` — server rejects it.
407
+ - Do **not** expect `firebase_id` from `/users/me`.
408
+ - `signing_key` comes from **`security/session`**, not profile.
409
+ - `sig` is a **server-minted** token from `security/token`, not a pure local encrypt of the jobs request.
410
+
411
+ ### 0.1.7
412
+
413
+ - Auto `POST /security/session` in `from_token` (`fetch_session=True`).
414
+ - `fetch_security_session` / `refresh_signing_key`.
415
+
416
+ ### 0.1.6
417
+
418
+ - Default **no** gateway `g-auth` on normal requests.
419
+ - Bearer + `g-device-id` + `g-username` + `t`.
420
+ - `/users/me` without `firebase_id`.
421
+
422
+ ### 0.1.5
423
+
424
+ - Gỡ helper theo platform (`get_instagram_job`, `get_tiktok_job`, …).
425
+ - Auth dùng chung: `from_token` + `get` / `post` / `put` / `delete` / `request` / `headers`.
426
+
427
+ ### 0.1.4
428
+
429
+ - `enable_sig` trên `GolikeAuth` / `from_token` (TikTok `sig`).
430
+
431
+ ### 0.1.3
432
+
433
+ - Header `sig` TikTok; gộp query vào path khi ký.
434
+
435
+ ### 0.1.2
436
+
437
+ - `from_token(token)` bootstrap `/users/me` + `firebase_id` (API cũ).
438
+
439
+ ### 0.1.1 / 0.1.0
440
+
441
+ - HKDF + AES-GCM `g-auth`, `g-device-id`, header `t`.
442
+
443
+ ### Nâng cấp
444
+
445
+ ```bash
446
+ pip install -U "gecmax[curl]"
447
+ ```
448
+
449
+ **Breaking (0.1.6):** mặc định **không** sinh `g-auth`. Code cũ dựa vào `auth.signing_key` / `auth.g_auth()` bắt buộc → truyền `enable_gauth=True` + `signing_key`, hoặc bỏ hẳn (API mới không cần).
450
+
451
+ **Breaking (0.1.5):** `get_tiktok_job` / `get_instagram_job` → dùng `auth.get` / `auth.post`.
452
+
453
+ ## Install
454
+
455
+ ```bash
456
+ pip install -U gecmax
457
+
458
+ # HTTP helper (curl_cffi) — khuyến nghị
459
+ pip install -U "gecmax[curl]"
460
+
461
+ # từ GitHub
462
+ pip install -U git+https://github.com/deno4908/golike-gauth.git
463
+ ```
464
+
465
+ `curl_cffi` là dependency tùy chọn cho HTTP helper. Cài extra `[curl]` để bảo đảm phiên bản hỗ trợ fingerprint Safari iOS được dùng đồng nhất; `curl_cffi>=0.9.0` cũng tương thích với Python 3.8.
466
+
467
+ ## Quick start
468
+
469
+ ### Chỉ cần token (API 2026 — khuyến dùng)
470
+
471
+ ```python
472
+ from xoichim import GolikeAuth
473
+
474
+ auth = GolikeAuth.from_token("eyJ...") # chỉ token
475
+
476
+ print(auth.user_id) # JWT sub
477
+ print(auth.username) # /users/me
478
+ print(auth.device_id) # UUID tự sinh
479
+ print(bool(auth.signing_key)) # security/session gweb key
480
+ print(bool(auth.gateway_signing_key)) # optional firebase_id gateway key
481
+ print(auth.profile) # raw /users/me
482
+ ```
483
+
484
+ #### Flow `from_token` làm gì?
485
+
486
+ | Bước | Nguồn | Kết quả |
487
+ |---|---|---|
488
+ | 1 | Decode JWT | `user_id` = `sub` |
489
+ | 2 | `GET /users/me` (UA mobile) | `username`, profile, optional `firebase_id` gateway key |
490
+ | 3 | `POST /api/v1/security/session` | `signing_key` riêng cho `gweb-v2` |
491
+ | 4 | `device_id` | UUID v4 (tự tạo) |
492
+ | 5 | Headers | Base headers; TikTok adds `sig` and conditionally gateway `g-auth` when `firebase_id` exists |
493
+
494
+ ```python
495
+ # Mặc định: tự handshake security/session
496
+ auth = GolikeAuth.from_token("eyJ...")
497
+
498
+ # Ép gateway g-auth với key riêng (hiếm)
499
+ auth = GolikeAuth.from_token(
500
+ "eyJ...",
501
+ signing_key="cxbbf6td1EXc...",
502
+ enable_gauth=True,
503
+ verify=True,
504
+ )
505
+ ```
506
+
507
+ #### Lấy token từ đâu?
508
+
509
+ Trên https://app.golike.net (đã login, F12 → Network):
510
+
511
+ - Request bất kỳ → header `Authorization: Bearer eyJ...`
512
+ - Hoặc Application / Local Storage / vuex (field `token`)
513
+
514
+ #### Ví dụ: lấy job Facebook chỉ với token
515
+
516
+ ```python
517
+ from xoichim import GolikeAuth
518
+
519
+ auth = GolikeAuth.from_token("eyJ...")
520
+
521
+ # list account FB tren Golike
522
+ accs = auth.get("/fb-account", params={"limit": 200}).json().get("data") or []
523
+ fb_id = accs[0]["fb_id"]
524
+
525
+ r = auth.get(
526
+ "/advertising/publishers/get-jobs-2026",
527
+ params={"fb_id": fb_id, "server": "sv2", "high_job": 1, "low_job": 1},
528
+ )
529
+ print(r.status_code, r.json())
530
+ ```
531
+
532
+ Script mẫu trong workspace: `test_fb_jobs.py` (chỉ hỏi token).
533
+
534
+ ### Thủ công (5 trường)
535
+
536
+ ```python
537
+ from xoichim import GolikeAuth
538
+
539
+ auth = GolikeAuth(
540
+ token="eyJ...", # JWT Bearer
541
+ signing_key="...", # security/session key cho TikTok sig
542
+ gateway_signing_key="...", # optional firebase_id cho gateway g-auth
543
+ user_id=123456, # JWT sub
544
+ username="your_username",
545
+ device_id="32484704-8a4e-4909-9d42-866773b321d6", # nên giữ cố định
546
+ )
547
+ ```
548
+
549
+ Lib dùng được với **mọi** path gateway. Chỉ cần đúng **method + path + body/query** như browser.
550
+
551
+ ### Cách dùng chung (khuyến nghị)
552
+
553
+ ```python
554
+ # GET — không body, params = query string
555
+ r = auth.get("/path/to/api", params={"key": "value"})
556
+ print(r.status_code, r.json())
557
+
558
+ # POST — có JSON body (g-auth ký đúng body compact, không space)
559
+ r = auth.post("/path/to/api", json={"a": 1, "b": "x"})
560
+ print(r.status_code, r.json())
561
+
562
+ # method bất kỳ
563
+ r = auth.request("PUT", "/path/to/api", json={...})
564
+ r = auth.request("DELETE", "/path/to/api")
565
+ ```
566
+
567
+ ### Ví dụ thật theo platform
568
+
569
+ #### Instagram — lấy job (GET)
570
+
571
+ ```python
572
+ r = auth.get(
573
+ "/advertising/publishers/instagram/jobs",
574
+ params={
575
+ "instagram_account_id": "966624",
576
+ "data": "null",
577
+ },
578
+ )
579
+ # hoặc helper:
580
+ r = auth.get_instagram_job("966624")
581
+ print(r.json())
582
+ ```
583
+
584
+ #### Instagram — skip job (POST, không phải GET!)
585
+
586
+ ```python
587
+ r = auth.post(
588
+ "/advertising/publishers/instagram/skip-jobs",
589
+ json={
590
+ "ads_id": 620978,
591
+ "object_id": "6155111723",
592
+ "account_id": 966624,
593
+ "type": "follow", # follow | like | comment | ...
594
+ },
595
+ )
596
+ # hoặc helper:
597
+ r = auth.skip_instagram_job(
598
+ ads_id=620978,
599
+ object_id="6155111723",
600
+ account_id=966624,
601
+ type="follow",
602
+ )
603
+ print(r.json())
604
+ ```
605
+
606
+ #### Instagram — complete job (POST)
607
+
608
+ ```python
609
+ r = auth.post(
610
+ "/advertising/publishers/instagram/complete-jobs",
611
+ json={
612
+ "instagram_users_advertising_id": 620978,
613
+ "instagram_account_id": 966624,
614
+ "async": True,
615
+ "data": None,
616
+ },
617
+ )
618
+ # hoặc helper:
619
+ r = auth.complete_instagram_job(
620
+ instagram_users_advertising_id=620978,
621
+ instagram_account_id=966624,
622
+ )
623
+ print(r.json())
624
+ ```
625
+
626
+ #### Twitter / X — lấy job (GET)
627
+
628
+ Tương đương curl:
629
+
630
+ `GET /api/advertising/publishers/twitter/jobs?account_id=97445`
631
+
632
+ ```python
633
+ r = auth.get(
634
+ "/advertising/publishers/twitter/jobs",
635
+ params={"account_id": "97445"},
636
+ )
637
+ print(r.status_code, r.json())
638
+ ```
639
+
640
+ #### TikTok / Facebook / … (cùng pattern)
641
+
642
+ ```python
643
+ # GET jobs (query tùy platform — copy từ Network tab browser)
644
+ r = auth.get(
645
+ "/advertising/publishers/tiktok/jobs",
646
+ params={"account_id": "123"}, # hoặc param khác tùy API
647
+ )
648
+
649
+ # POST skip / complete — luôn dùng auth.post(..., json={...})
650
+ r = auth.post(
651
+ "/advertising/publishers/tiktok/skip-jobs",
652
+ json={...}, # body copy từ Network tab
653
+ )
654
+ ```
655
+
656
+ #### Users / endpoint khác
657
+
658
+ ```python
659
+ r = auth.get("/users/me")
660
+ r = auth.post("/some/path", json={"foo": "bar"})
661
+ ```
662
+
663
+ ### Sai thường gặp
664
+
665
+ ```python
666
+ # ❌ SAI: auth.post() đã gọi API, không phải headers
667
+ # ❌ SAI: skip-jobs dùng GET → 405
668
+ from curl_cffi import requests as curl_requests
669
+
670
+ response = curl_requests.get(
671
+ "https://gateway.golike.net/api/advertising/publishers/instagram/skip-jobs",
672
+ headers=auth.post("/advertising/publishers/instagram/skip-jobs", json={...}),
673
+ )
674
+
675
+ # ✅ ĐÚNG
676
+ response = auth.post(
677
+ "/advertising/publishers/instagram/skip-jobs",
678
+ json={
679
+ "ads_id": 620978,
680
+ "object_id": "6155111723",
681
+ "account_id": 966624,
682
+ "type": "follow",
683
+ },
684
+ )
685
+ print(response.json())
686
+ ```
687
+
688
+ Nếu tự gửi HTTP request bằng `curl_cffi`, **method + body bytes** phải khớp lúc ký:
689
+
690
+ ```python
691
+ import json
692
+ from curl_cffi import requests as curl_requests
693
+
694
+ body = {
695
+ "ads_id": 620978,
696
+ "object_id": "6155111723",
697
+ "account_id": 966624,
698
+ "type": "follow",
699
+ }
700
+ raw = json.dumps(body, separators=(",", ":"), ensure_ascii=False) # không space
701
+ headers = auth.headers(
702
+ "POST",
703
+ "/advertising/publishers/instagram/skip-jobs",
704
+ body=raw,
705
+ )
706
+ response = curl_requests.post(
707
+ "https://gateway.golike.net/api/advertising/publishers/instagram/skip-jobs",
708
+ headers=headers,
709
+ data=raw.encode("utf-8"),
710
+ )
711
+ ```
712
+
713
+ ### Headers only / low-level
714
+
715
+ ```python
716
+ from gecmax import generate_g_auth, generate_device_id, decode_g_auth
717
+
718
+ # headers: method/path/body phải trùng request thật
719
+ headers = auth.headers(
720
+ "GET",
721
+ "/advertising/publishers/twitter/jobs",
722
+ body="", # GET không body
723
+ )
724
+
725
+ g_auth = generate_g_auth(
726
+ method="GET",
727
+ path="/advertising/publishers/twitter/jobs",
728
+ body="",
729
+ signing_key="...",
730
+ device_id=generate_device_id(),
731
+ user_id=123456,
732
+ )
733
+ print(decode_g_auth(g_auth, "..."))
734
+ ```
735
+
736
+ ### Method cheatsheet
737
+
738
+ | API | Method | Params / Body |
739
+ |---|---|---|
740
+ | `/advertising/publishers/instagram/jobs` | **GET** | query: `instagram_account_id`, `data` |
741
+ | `/advertising/publishers/instagram/skip-jobs` | **POST** | JSON: `{ads_id, object_id, account_id, type}` |
742
+ | `/advertising/publishers/instagram/complete-jobs` | **POST** | JSON: `{instagram_users_advertising_id, instagram_account_id, async, data, ...}` |
743
+ | `/advertising/publishers/twitter/jobs` | **GET** | query: `account_id` |
744
+ | `/users/me` | **GET** | — |
745
+ | path khác | copy từ browser Network | **đúng method + body như browser** |
746
+
747
+ Quy tắc:
748
+
749
+ 1. **GET** → `auth.get(path, params=...)` — body ký = `""`
750
+ 2. **POST** → `auth.post(path, json=...)` — body ký = JSON compact
751
+ 3. **405** = sai method (vd. GET `skip-jobs`)
752
+ 4. **g-auth** tạo mới **mỗi request**, khớp method + path + body
753
+ 5. Path truyền vào **không** cần `/api` prefix (`auth` tự thêm base `.../api`)
754
+
755
+ ## Where to get `signing_key`
756
+
757
+ On https://app.golike.net (logged in), DevTools console:
758
+
759
+ ```js
760
+ document.querySelector('#app').__vue__.$store.state.signing_key
761
+ ```
762
+
763
+ > This value may differ from `data.firebase_id` in `/users/me`. Always use the store key the browser actually signs with.
764
+
765
+ ## CLI
766
+
767
+ ```bash
768
+ golike-gauth \
769
+ --token eyJ... \
770
+ --signing-key ... \
771
+ --user-id 123456 \
772
+ --username your_username \
773
+ --call --ig-account-id YOUR_IG_ACCOUNT_ID
774
+ ```
775
+
776
+ ## Notes
777
+
778
+ - `g-auth` must be **regenerated for every request** (binds method + path + body hash + timestamp).
779
+ - GET requests sign body as empty string `""`.
780
+ - Path signed is pathname only, e.g. `/api/advertising/publishers/instagram/jobs`.
781
+
782
+ ## Development
783
+
784
+ ```bash
785
+ git clone https://github.com/deno4908/golike-gauth.git
786
+ cd golike-gauth
787
+ python -m pip install -e ".[dev,curl]"
788
+ ```
789
+
790
+ ### Build & publish (PyPI)
791
+
792
+ ```bash
793
+ python -m pip install -U build twine
794
+ python -m build
795
+ python -m twine upload dist/*
796
+ ```
797
+
798
+ ## Contributing
799
+
800
+ Issues and PRs: https://github.com/deno4908/golike-gauth/issues
801
+
802
+ ## License
803
+
804
+ [MIT](LICENSE) — see [LICENSE](https://github.com/deno4908/golike-gauth/blob/main/LICENSE)
805
+
806
+ ---
807
+
808
+ # Phân tích mã hóa / giải mã `g-auth` (Golike Gateway)
809
+
810
+ Tài liệu này mô tả cách app web Golike tạo header **`g-auth`** cho mỗi request tới `gateway.golike.net`, và cách server (hoặc client debug) giải mã token đó.
811
+
812
+ Nguồn reverse: bundle `index-68ef440b.js` (các hàm `H_`, `q_`, `Mg`, `M_`, `j_`, `Jd`, `aA`, `bu`).
813
+
814
+ ---
815
+
816
+ ## 1. Vai trò của `g-auth`
817
+
818
+ `g-auth` là **request-binding token**: mỗi request HTTP mang một token mới, gắn với:
819
+
820
+ | Trường payload | Ý nghĩa |
821
+ |---|---|
822
+ | `n` | HTTP method (`GET` / `POST` / …) |
823
+ | `k` | Pathname API (không có query) |
824
+ | `q` | SHA-256 hex của body |
825
+ | `d` | `g-device-id` (UUID) |
826
+ | `u` | `user_id` |
827
+ | `t` | timestamp client (ms) |
828
+ | `x` | nonce ngẫu nhiên |
829
+ | `r` | digest ngắn chống giả mạo thô |
830
+
831
+ Token được **mã hóa AES-GCM** bằng khóa dẫn xuất từ `signing_key` của user. Server giải mã → kiểm tra method/path/body/device/user/time → cho phép request.
832
+
833
+ > `g-auth` **không tái sử dụng** được giữa các request (path/body/time khác nhau, và có nonce).
834
+
835
+ ---
836
+
837
+ ## 2. Các header liên quan
838
+
839
+ Ngoài JWT `Authorization: Bearer …`, client còn gửi:
840
+
841
+ | Header | Nguồn / công thức |
842
+ |---|---|
843
+ | `g-auth` | AES-GCM token (mục 4–5) |
844
+ | `g-device-id` | UUID v4, lưu `localStorage.device_id` |
845
+ | `g-username` | username user |
846
+ | `g-version` | version app, ví dụ `26.07.10.2` |
847
+ | `g-client` | client id app |
848
+ | `t` | `btoa(btoa(btoa(unix_seconds)))` — timestamp 3 lớp Base64 |
849
+
850
+ ---
851
+
852
+ ## 3. `signing_key` — nguyên liệu khóa
853
+
854
+ ### 3.1. Lấy key ở đâu?
855
+
856
+ Trong Vuex store:
857
+
858
+ ```js
859
+ $store.state.signing_key
860
+ // DevTools:
861
+ document.querySelector('#app').__vue__.$store.state.signing_key
862
+ ```
863
+
864
+ > **Lưu ý thực tế:** giá trị store **có thể khác** `data.firebase_id` trả về từ `GET /users/me`.
865
+ > Luôn dùng đúng key mà browser đang ký (store / console), không chỉ dựa vào field API.
866
+
867
+ Key là **32 bytes**, thường encode **Base64** (có `+`, `/`, `=`), ví dụ:
868
+
869
+ ```text
870
+ cxbbf6td1EXcoEWlnk0eVmJwG1NJYhiqPxNcUXG+cBc=
871
+ ```
872
+
873
+ Client cũng chấp nhận hex 64 ký tự.
874
+
875
+ ### 3.2. Parse key (`k_`)
876
+
877
+ ```
878
+ nếu chuỗi là hex hợp lệ và decode ra 32 bytes → dùng
879
+ else decode Base64 / Base64URL → 32 bytes → dùng
880
+ else lỗi: signing key must decode to 32 bytes
881
+ ```
882
+
883
+ ---
884
+
885
+ ## 4. Dẫn xuất khóa AES (HKDF)
886
+
887
+ Trước khi AES-GCM, raw key 32 bytes được đưa qua **HKDF-SHA256**:
888
+
889
+ | Tham số | Giá trị |
890
+ |---|---|
891
+ | IKM | 32 bytes từ `signing_key` |
892
+ | Hash | SHA-256 |
893
+ | Salt | `glk-gauth-v3-2026q3` |
894
+ | Info | `aes-gcm-key` |
895
+ | Output length | 32 bytes |
896
+
897
+ ```
898
+ AES_KEY = HKDF-SHA256(
899
+ ikm = decode(signing_key),
900
+ salt = "glk-gauth-v3-2026q3",
901
+ info = "aes-gcm-key",
902
+ len = 32
903
+ )
904
+ ```
905
+
906
+ Hằng số trong JS:
907
+
908
+ ```js
909
+ B_ = "glk-gauth"
910
+ E_ = "v3-2026" // sn(247)
911
+ S_ = "q3"
912
+ Sg = B_ + "-" + E_ + S_ // => "glk-gauth-v3-2026q3"
913
+ I_ = "aes-gcm-key"
914
+ ```
915
+
916
+ `AES_KEY` import vào WebCrypto / OpenSSL dưới dạng **AES-256-GCM**.
917
+
918
+ ---
919
+
920
+ ## 5. Tạo payload trước khi mã hóa
921
+
922
+ ### 5.1. Chuẩn hóa path (`Pg` + `yu`)
923
+
924
+ - Bỏ query (`?...`) và hash (`#...`)
925
+ - Ghép với base `https://gateway.golike.net/api`
926
+ - Chỉ lấy **pathname**
927
+
928
+ Ví dụ:
929
+
930
+ ```text
931
+ request URL:
932
+ /advertising/publishers/instagram/jobs?instagram_account_id=966624&data=null
933
+
934
+ signed path k:
935
+ /api/advertising/publishers/instagram/jobs
936
+ ```
937
+
938
+ ### 5.2. Hash body (`j_`)
939
+
940
+ ```text
941
+ body_str =
942
+ null/undefined → ""
943
+ string → chính nó
944
+ object → JSON.stringify(obj) // compact, không space
945
+ // JS: JSON.stringify → {"a":1}
946
+
947
+ q = SHA256_hex(body_str)
948
+ ```
949
+
950
+ **GET / HEAD / DELETE** trong app: body ký = chuỗi rỗng `""`
951
+
952
+ ```text
953
+ q = SHA256("")
954
+ = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
955
+ ```
956
+
957
+ ### 5.3. Digest `r` (`M_`)
958
+
959
+ ```text
960
+ r = SHA256_hex( f"{t}:{device_id}:{q}:{salt}" )[0:16]
961
+ ```
962
+
963
+ với `salt = glk-gauth-v3-2026q3`, `t` là timestamp **milliseconds**.
964
+
965
+ ### 5.4. Object payload đầy đủ
966
+
967
+ Thứ tự key (quan trọng vì `JSON.stringify` giữ insertion order):
968
+
969
+ ```json
970
+ {
971
+ "t": 1783944428398,
972
+ "x": "z7Yv5E5ClYSc0gyyjllQ2w",
973
+ "d": "32484704-8a4e-4909-9d42-866773b321d6",
974
+ "u": 639111,
975
+ "n": "GET",
976
+ "k": "/api/advertising/publishers/instagram/jobs",
977
+ "q": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
978
+ "r": "286c4f5bd68ec1c4"
979
+ }
980
+ ```
981
+
982
+ | Field | Mô tả |
983
+ |---|---|
984
+ | `t` | `Date.now()` (ms) |
985
+ | `x` | 16 bytes random → Base64URL (no padding) |
986
+ | `d` | device UUID (`g-device-id`) |
987
+ | `u` | user id (number) |
988
+ | `n` | method upper-case |
989
+ | `k` | pathname đã chuẩn hóa |
990
+ | `q` | body hash |
991
+ | `r` | digest 16 hex chars |
992
+
993
+ Chuỗi plaintext:
994
+
995
+ ```text
996
+ plaintext = JSON.stringify(payload) // separators mặc định JS: không space
997
+ → UTF-8 bytes
998
+ ```
999
+
1000
+ ---
1001
+
1002
+ ## 6. Mã hóa (encrypt) — `H_`
1003
+
1004
+ ```
1005
+ IV = 12 bytes random (crypto.getRandomValues)
1006
+ CT = AES-256-GCM.Encrypt(
1007
+ key = AES_KEY,
1008
+ iv = IV,
1009
+ pt = plaintext_utf8
1010
+ )
1011
+ // CT = ciphertext || 16-byte auth tag (WebCrypto / cryptography)
1012
+
1013
+ token_bytes = IV || CT
1014
+ g-auth = Base64URL(token_bytes) // bỏ padding '='
1015
+ ```
1016
+
1017
+ Sơ đồ:
1018
+
1019
+ ```
1020
+ signing_key (b64/hex)
1021
+ │
1022
+ ▼ decode 32B
1023
+ IKM
1024
+ │
1025
+ ▼ HKDF-SHA256(salt, info)
1026
+ AES_KEY (32B)
1027
+ │
1028
+ │ ┌─ method, path, body, device, user, now ─┐
1029
+ │ ▼ │
1030
+ │ JSON payload {t,x,d,u,n,k,q,r} │
1031
+ │ ▼ UTF-8 │
1032
+ │ plaintext ──────────┐ │
1033
+ │ │ │
1034
+ ▼ ▼ │
1035
+ AES-256-GCM(IV=12B) ←── encrypt │
1036
+ │ │
1037
+ ▼ │
1038
+ IV || ciphertext+tag │
1039
+ │ │
1040
+ ▼ Base64URL │
1041
+ header g-auth ───────────────────────────────────┘
1042
+ ```
1043
+
1044
+ ---
1045
+
1046
+ ## 7. Giải mã (decrypt) — `q_` / server / debug
1047
+
1048
+ ```
1049
+ raw = Base64URL_decode(g-auth)
1050
+ IV = raw[0:12]
1051
+ CT = raw[12:] // ciphertext + tag
1052
+
1053
+ plaintext = AES-256-GCM.Decrypt(AES_KEY, IV, CT)
1054
+ payload = JSON.parse(plaintext)
1055
+ ```
1056
+
1057
+ Sau khi giải mã, server thường kiểm tra:
1058
+
1059
+ 1. **Decrypt OK** (key + IV + tag đúng)
1060
+ - Fail → `decrypt_fail_check_signing_key_or_iv_or_tag_or_r_field`
1061
+ 2. **`n`** khớp HTTP method
1062
+ 3. **`k`** khớp pathname canonical (vd. `/api/...`)
1063
+ 4. **`q`** khớp SHA256 body thực tế nhận được
1064
+ 5. **`d` / `u`** khớp session / JWT
1065
+ 6. **`t`** không lệch quá xa server time (`ts_drift_ms`)
1066
+ 7. **`r`** khớp lại công thức digest
1067
+
1068
+ Endpoint debug hữu ích:
1069
+
1070
+ ```http
1071
+ POST /api/security/echo
1072
+ ```
1073
+
1074
+ Response có block `gauth` mô tả lỗi decode / match.
1075
+
1076
+ ---
1077
+
1078
+ ## 8. Ví dụ end-to-end (GET jobs)
1079
+
1080
+ ### Request
1081
+
1082
+ ```http
1083
+ GET /api/advertising/publishers/instagram/jobs?instagram_account_id=966624&data=null
1084
+ Authorization: Bearer <jwt>
1085
+ g-device-id: 32484704-8a4e-4909-9d42-866773b321d6
1086
+ g-username: vinhhacker
1087
+ g-auth: Zmo3WFa5lp2X...
1088
+ t: VFZSak5FMTZhekJPUkZGNVQwRTlQUT09
1089
+ ```
1090
+
1091
+ ### Payload đã giải mã (thực tế từ browser)
1092
+
1093
+ ```json
1094
+ {
1095
+ "t": 1783944428398,
1096
+ "x": "z7Yv5E5ClYSc0gyyjllQ2w",
1097
+ "d": "32484704-8a4e-4909-9d42-866773b321d6",
1098
+ "u": 639111,
1099
+ "n": "GET",
1100
+ "k": "/api/advertising/publishers/instagram/jobs",
1101
+ "q": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1102
+ "r": "286c4f5bd68ec1c4"
1103
+ }
1104
+ ```
1105
+
1106
+ ### Tính lại `q` và `r`
1107
+
1108
+ ```text
1109
+ q = SHA256("")
1110
+ = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1111
+
1112
+ r = SHA256(
1113
+ "1783944428398"
1114
+ + ":" + "32484704-8a4e-4909-9d42-866773b321d6"
1115
+ + ":" + q
1116
+ + ":" + "glk-gauth-v3-2026q3"
1117
+ )[:16]
1118
+ = 286c4f5bd68ec1c4
1119
+ ```
1120
+
1121
+ ---
1122
+
1123
+ ## 9. Header `t` (không nằm trong AES)
1124
+
1125
+ ```js
1126
+ t = btoa(btoa(btoa(String(unix_seconds))))
1127
+ ```
1128
+
1129
+ Ví dụ:
1130
+
1131
+ ```text
1132
+ unix = 1783944424
1133
+ → btoa × 3 → "VFZSak5FMTZhekJPUkZGNVQwRTlQUT09"
1134
+ ```
1135
+
1136
+ Đây là timestamp phụ (giây), tách biệt field `t` (ms) bên trong `g-auth`.
1137
+
1138
+ ---
1139
+
1140
+ ## 10. Pseudocode Python
1141
+
1142
+ ```python
1143
+ import base64, hashlib, json, secrets, time
1144
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
1145
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
1146
+ from cryptography.hazmat.primitives import hashes
1147
+
1148
+ SALT = "glk-gauth-v3-2026q3"
1149
+ INFO = "aes-gcm-key"
1150
+
1151
+ def b64url(data: bytes) -> str:
1152
+ return base64.urlsafe_b64encode(data).decode().rstrip("=")
1153
+
1154
+ def sha256_hex(s: str) -> str:
1155
+ return hashlib.sha256(s.encode()).hexdigest()
1156
+
1157
+ def derive_key(signing_key_b64: str) -> bytes:
1158
+ ikm = base64.b64decode(signing_key_b64)
1159
+ return HKDF(
1160
+ algorithm=hashes.SHA256(),
1161
+ length=32,
1162
+ salt=SALT.encode(),
1163
+ info=INFO.encode(),
1164
+ ).derive(ikm)
1165
+
1166
+ def encrypt_g_auth(method, path, body, signing_key, device_id, user_id) -> str:
1167
+ key = derive_key(signing_key)
1168
+ iv = secrets.token_bytes(12)
1169
+ t = int(time.time() * 1000)
1170
+ q = sha256_hex(body if body is not None else "")
1171
+ r = sha256_hex(f"{t}:{device_id}:{q}:{SALT}")[:16]
1172
+ payload = {
1173
+ "t": t,
1174
+ "x": b64url(secrets.token_bytes(16)),
1175
+ "d": device_id,
1176
+ "u": user_id,
1177
+ "n": method.upper(),
1178
+ "k": path,
1179
+ "q": q,
1180
+ "r": r,
1181
+ }
1182
+ pt = json.dumps(payload, separators=(",", ":")).encode()
1183
+ ct = AESGCM(key).encrypt(iv, pt, None) # ct + tag
1184
+ return b64url(iv + ct)
1185
+
1186
+ def decrypt_g_auth(token: str, signing_key: str) -> dict:
1187
+ key = derive_key(signing_key)
1188
+ pad = "=" * (-len(token) % 4)
1189
+ raw = base64.urlsafe_b64decode(token.replace("-", "+").replace("_", "/") + pad)
1190
+ iv, ct = raw[:12], raw[12:]
1191
+ pt = AESGCM(key).decrypt(iv, ct, None)
1192
+ return json.loads(pt)
1193
+ ```
1194
+
1195
+ ---
1196
+
1197
+ ## 11. Lỗi thường gặp
1198
+
1199
+ | Triệu chứng | Nguyên nhân |
1200
+ |---|---|
1201
+ | `decrypt_fail_...` | Sai `signing_key`, hỏng token, hoặc không đúng thuật toán HKDF/AES |
1202
+ | `AUTH_MISSING` | Không gửi `g-auth` (và/hoặc header bắt buộc khác) |
1203
+ | `403` “cập nhật phiên bản…” | Token thiếu/sai trên endpoint jobs; server từ chối client “cũ/không ký” |
1204
+ | 200 ở browser, 403 ở script | Reuse `g-auth` cũ; hoặc path/body hash khác (space trong JSON, thiếu `/api`, …) |
1205
+ | Decode local OK nhưng server fail | Key local ≠ key server đang expect; lấy lại từ `store.state.signing_key` |
1206
+
1207
+ ---
1208
+
1209
+ ## 12. Checklist implement đúng
1210
+
1211
+ 1. `signing_key` = **store browser**, decode ra đúng 32 bytes
1212
+ 2. HKDF salt = `glk-gauth-v3-2026q3`, info = `aes-gcm-key`
1213
+ 3. Path ký = pathname có prefix `/api/...`, **không** query
1214
+ 4. GET body ký = `""`
1215
+ 5. POST body ký = **cùng bytes** body gửi đi (`JSON` compact nếu stringify)
1216
+ 6. Payload JSON **không space**, đúng thứ tự key `t,x,d,u,n,k,q,r`
1217
+ 7. AES-GCM IV 12 bytes, output `Base64URL(IV || CT||TAG)`
1218
+ 8. **Mỗi request** tạo `g-auth` mới
1219
+
1220
+ ---
1221
+
1222
+ ## 13. Tóm tắt một dòng
1223
+
1224
+ > **`g-auth` = Base64URL( IV₁₂ ‖ AES-256-GCM<sub>HKDF(signing_key)</sub>( JSON{method, path, bodyHash, device, user, time, nonce, digest} ) )**