playttm 0.0.4 → 0.0.6

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/dist/lib.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ export declare const isBrowserEnvironment: () => boolean;
2
+ /**
3
+ * Extract transaction amount from EMVCo / ThaiQR payload (Tag 54)
4
+ */
5
+ export declare const parseThaiQRAmount: (qrCode?: string) => string | null;
1
6
  export interface TTMConfig {
2
7
  cookies?: string;
3
8
  headers?: Record<string, string>;
@@ -5,6 +10,8 @@ export interface TTMConfig {
5
10
  timeout?: number;
6
11
  endpoint?: string;
7
12
  onProgress?: (step: string, message: string, data?: any) => void;
13
+ isBrowser?: boolean;
14
+ adapter?: any;
8
15
  }
9
16
  export interface RoundInfo {
10
17
  id: string;
@@ -71,11 +78,24 @@ export interface QuickBookResult {
71
78
  }
72
79
  export declare class ThaiTicketMajor {
73
80
  private client;
74
- private jar;
81
+ private jar?;
75
82
  private endpoint;
76
83
  private defaultHeaders;
77
84
  private onProgressCallback?;
85
+ inBrowser: boolean;
86
+ private rawCookies?;
78
87
  constructor(config?: TTMConfig);
88
+ static readonly BOT_PROTECTION_COOKIES: Set<string>;
89
+ /**
90
+ * Synchronize a cookie string into Chrome Extension's cookie store (chrome.cookies API).
91
+ * Automatically ignores bot protection cookies (Akamai _abck, bm_*, WAF) to prevent Access Denied (403) bans.
92
+ */
93
+ static syncCookiesToChrome(cookieString: string, domainUrl?: string): Promise<void>;
94
+ /**
95
+ * Setup declarativeNetRequest session rules in Chrome Extension to set Referer header for API requests only.
96
+ */
97
+ static setupExtensionRules(): Promise<void>;
98
+ syncCookies(domainUrl?: string): Promise<void>;
79
99
  private emitProgress;
80
100
  /**
81
101
  * Step 1: ดึงรอบการแสดงทั้งหมดจากหน้าหลักคอนเสิร์ต
package/dist/lib.js CHANGED
@@ -45,30 +45,202 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.displayQRCode = exports.ThaiTicketMajor = void 0;
48
+ exports.displayQRCode = exports.ThaiTicketMajor = exports.parseThaiQRAmount = exports.isBrowserEnvironment = void 0;
49
49
  const axios_1 = __importDefault(require("axios"));
50
50
  const cheerio = __importStar(require("cheerio"));
51
51
  const axios_cookiejar_support_1 = require("axios-cookiejar-support");
52
52
  const tough_cookie_1 = require("tough-cookie");
53
+ const isBrowserEnvironment = () => {
54
+ var _a, _b;
55
+ if (typeof window !== "undefined" && typeof window.document !== "undefined") {
56
+ return true;
57
+ }
58
+ if (typeof globalThis !== "undefined" && Boolean((_b = (_a = globalThis.chrome) === null || _a === void 0 ? void 0 : _a.runtime) === null || _b === void 0 ? void 0 : _b.id)) {
59
+ return true;
60
+ }
61
+ if (typeof process === "undefined" || !process.versions || !process.versions.node) {
62
+ return true;
63
+ }
64
+ return false;
65
+ };
66
+ exports.isBrowserEnvironment = isBrowserEnvironment;
67
+ /**
68
+ * Extract transaction amount from EMVCo / ThaiQR payload (Tag 54)
69
+ */
70
+ const parseThaiQRAmount = (qrCode) => {
71
+ if (!qrCode)
72
+ return null;
73
+ const match = qrCode.match(/(?:^|[^0-9])54(\d{2})([0-9.]+)/);
74
+ if (match) {
75
+ const len = parseInt(match[1], 10);
76
+ const amountStr = match[2].substring(0, len);
77
+ if (!isNaN(parseFloat(amountStr))) {
78
+ return amountStr;
79
+ }
80
+ }
81
+ return null;
82
+ };
83
+ exports.parseThaiQRAmount = parseThaiQRAmount;
53
84
  // ==========================================
54
85
  // Main Library Class
55
86
  // ==========================================
56
87
  class ThaiTicketMajor {
57
88
  constructor(config = {}) {
58
- this.jar = new tough_cookie_1.CookieJar();
89
+ this.inBrowser =
90
+ typeof config.isBrowser === "boolean"
91
+ ? config.isBrowser
92
+ : (0, exports.isBrowserEnvironment)();
93
+ this.rawCookies = config.cookies;
59
94
  this.endpoint = config.endpoint || "https://booking.thaiticketmajor.com/booking/3m";
60
95
  this.onProgressCallback = config.onProgress;
61
96
  const defaultUA = config.userAgent ||
62
97
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
63
- this.defaultHeaders = Object.assign({ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "accept-language": "th,en-US;q=0.9,en;q=0.8", "user-agent": defaultUA, referer: "https://booking.thaiticketmajor.com" }, (config.headers || {}));
64
- if (config.cookies) {
65
- this.defaultHeaders["cookie"] = config.cookies;
98
+ this.defaultHeaders = Object.assign({ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "accept-language": "th,en-US;q=0.9,en;q=0.8" }, (config.headers || {}));
99
+ if (!this.inBrowser) {
100
+ this.jar = new tough_cookie_1.CookieJar();
101
+ this.defaultHeaders["user-agent"] = defaultUA;
102
+ this.defaultHeaders["referer"] = "https://booking.thaiticketmajor.com";
103
+ if (config.cookies) {
104
+ this.defaultHeaders["cookie"] = config.cookies;
105
+ }
106
+ this.client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create(Object.assign({ jar: this.jar, withCredentials: true, timeout: config.timeout || 30000 }, (config.adapter ? { adapter: config.adapter } : {}))));
107
+ }
108
+ else {
109
+ // Browser / Chrome Extension environment
110
+ this.client = axios_1.default.create(Object.assign({ withCredentials: true, timeout: config.timeout || 30000 }, (config.adapter ? { adapter: config.adapter } : {})));
111
+ // Interceptor to strip unsafe headers forbidden by browsers (Fetch / XHR specification)
112
+ const unsafeHeaders = ["user-agent", "referer", "cookie", "origin", "host", "connection"];
113
+ this.client.interceptors.request.use((reqConfig) => {
114
+ if (reqConfig.headers) {
115
+ for (const header of unsafeHeaders) {
116
+ delete reqConfig.headers[header];
117
+ delete reqConfig.headers[header.toLowerCase()];
118
+ delete reqConfig.headers[header.toUpperCase()];
119
+ const titleCase = header.charAt(0).toUpperCase() + header.slice(1);
120
+ delete reqConfig.headers[titleCase];
121
+ }
122
+ }
123
+ return reqConfig;
124
+ });
66
125
  }
67
- this.client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
68
- jar: this.jar,
69
- withCredentials: true,
70
- timeout: config.timeout || 30000,
71
- }));
126
+ }
127
+ /**
128
+ * Synchronize a cookie string into Chrome Extension's cookie store (chrome.cookies API).
129
+ * Automatically ignores bot protection cookies (Akamai _abck, bm_*, WAF) to prevent Access Denied (403) bans.
130
+ */
131
+ static syncCookiesToChrome(cookieString_1) {
132
+ return __awaiter(this, arguments, void 0, function* (cookieString, domainUrl = "https://booking.thaiticketmajor.com") {
133
+ var _a;
134
+ if (typeof chrome === "undefined" || !((_a = chrome === null || chrome === void 0 ? void 0 : chrome.cookies) === null || _a === void 0 ? void 0 : _a.set)) {
135
+ return;
136
+ }
137
+ const pairs = cookieString.split(";");
138
+ for (const pair of pairs) {
139
+ const trimmed = pair.trim();
140
+ if (!trimmed)
141
+ continue;
142
+ const eqIndex = trimmed.indexOf("=");
143
+ if (eqIndex === -1)
144
+ continue;
145
+ const name = trimmed.substring(0, eqIndex).trim();
146
+ const value = trimmed.substring(eqIndex + 1).trim();
147
+ if (!name)
148
+ continue;
149
+ // DO NOT overwrite Akamai / WAF bot tokens!
150
+ // Writing stale _abck or bm_* triggers Akamai Bot Protection -> Access Denied.
151
+ if (ThaiTicketMajor.BOT_PROTECTION_COOKIES.has(name.toLowerCase())) {
152
+ continue;
153
+ }
154
+ try {
155
+ yield chrome.cookies.set({
156
+ url: domainUrl,
157
+ name,
158
+ value,
159
+ domain: ".thaiticketmajor.com",
160
+ path: "/",
161
+ });
162
+ }
163
+ catch (_b) {
164
+ try {
165
+ yield chrome.cookies.set({
166
+ url: domainUrl,
167
+ name,
168
+ value,
169
+ path: "/",
170
+ });
171
+ }
172
+ catch (_c) { }
173
+ }
174
+ }
175
+ });
176
+ }
177
+ /**
178
+ * Setup declarativeNetRequest session rules in Chrome Extension to set Referer header for API requests only.
179
+ */
180
+ static setupExtensionRules() {
181
+ return __awaiter(this, void 0, void 0, function* () {
182
+ var _a, _b, _c, _d, _e, _f, _g;
183
+ if (typeof chrome === "undefined" || !((_a = chrome === null || chrome === void 0 ? void 0 : chrome.declarativeNetRequest) === null || _a === void 0 ? void 0 : _a.updateSessionRules)) {
184
+ return;
185
+ }
186
+ try {
187
+ yield chrome.declarativeNetRequest.updateSessionRules({
188
+ removeRuleIds: [1001, 1002],
189
+ addRules: [
190
+ {
191
+ id: 1001,
192
+ priority: 1,
193
+ action: {
194
+ type: ((_b = chrome.declarativeNetRequest.RuleActionType) === null || _b === void 0 ? void 0 : _b.MODIFY_HEADERS) || "modifyHeaders",
195
+ requestHeaders: [
196
+ {
197
+ header: "Referer",
198
+ operation: ((_c = chrome.declarativeNetRequest.HeaderOperation) === null || _c === void 0 ? void 0 : _c.SET) || "set",
199
+ value: "https://booking.thaiticketmajor.com",
200
+ },
201
+ ],
202
+ },
203
+ condition: {
204
+ urlFilter: "thaiticketmajor.com",
205
+ resourceTypes: [
206
+ ((_d = chrome.declarativeNetRequest.ResourceType) === null || _d === void 0 ? void 0 : _d.XMLHTTPREQUEST) || "xmlhttprequest",
207
+ ],
208
+ },
209
+ },
210
+ {
211
+ id: 1002,
212
+ priority: 1,
213
+ action: {
214
+ type: ((_e = chrome.declarativeNetRequest.RuleActionType) === null || _e === void 0 ? void 0 : _e.MODIFY_HEADERS) || "modifyHeaders",
215
+ requestHeaders: [
216
+ {
217
+ header: "Referer",
218
+ operation: ((_f = chrome.declarativeNetRequest.HeaderOperation) === null || _f === void 0 ? void 0 : _f.SET) || "set",
219
+ value: "https://kpaymentgateway.kasikornbank.com/",
220
+ },
221
+ ],
222
+ },
223
+ condition: {
224
+ urlFilter: "kasikornbank.com",
225
+ resourceTypes: [
226
+ ((_g = chrome.declarativeNetRequest.ResourceType) === null || _g === void 0 ? void 0 : _g.XMLHTTPREQUEST) || "xmlhttprequest",
227
+ ],
228
+ },
229
+ },
230
+ ],
231
+ });
232
+ }
233
+ catch (err) {
234
+ console.warn("Could not set declarativeNetRequest session rules:", err);
235
+ }
236
+ });
237
+ }
238
+ syncCookies(domainUrl) {
239
+ return __awaiter(this, void 0, void 0, function* () {
240
+ if (this.rawCookies) {
241
+ yield ThaiTicketMajor.syncCookiesToChrome(this.rawCookies, domainUrl);
242
+ }
243
+ });
72
244
  }
73
245
  emitProgress(step, message, data) {
74
246
  if (this.onProgressCallback) {
@@ -641,11 +813,19 @@ class ThaiTicketMajor {
641
813
  */
642
814
  quickBook(options) {
643
815
  return __awaiter(this, void 0, void 0, function* () {
644
- var _a, _b, _c, _d;
816
+ var _a, _b, _c, _d, _e;
645
817
  const { eventUrl, roundIndex = 0, roundId, zoneId, ticketCount = 1, seatSelection = "random", attendees = [], deliveryType = "1", payType = "KBQR", paymentDelayMs = 1500, } = options;
646
818
  if (options.onProgress) {
647
819
  this.onProgressCallback = options.onProgress;
648
820
  }
821
+ // In Chrome Extension environment, prepare cookies and network rules
822
+ if (this.inBrowser) {
823
+ this.emitProgress("INIT_EXTENSION", "Preparing Chrome extension cookies and network rules...");
824
+ if (this.rawCookies) {
825
+ yield this.syncCookies();
826
+ }
827
+ yield ThaiTicketMajor.setupExtensionRules();
828
+ }
649
829
  // 1. ดึงรอบการแสดง
650
830
  const rounds = yield this.getRounds(eventUrl);
651
831
  if (rounds.length === 0) {
@@ -762,7 +942,14 @@ class ThaiTicketMajor {
762
942
  const orderEncData = yield this.orderEncKBankQR(zoneData.k, payCfmData);
763
943
  const kbankInfo = yield this.getKBankQR(orderEncData);
764
944
  const kbankAuth = yield this.paymentKBankQR(orderEncData, kbankInfo);
765
- const qrResult = yield this.generateThaiQR(kbankAuth.orderId, kbankAuth.apiKey, orderEncData.amount);
945
+ const targetAmount = orderEncData.transactionamount ||
946
+ orderEncData.totalamount ||
947
+ orderEncData.amount ||
948
+ payCfmData.amount ||
949
+ kbankInfo.totalamount ||
950
+ paymentAllData["cal_totalamount"] ||
951
+ "0";
952
+ const qrResult = yield this.generateThaiQR(kbankAuth.orderId, kbankAuth.apiKey, targetAmount);
766
953
  const qrRawCode = (qrResult === null || qrResult === void 0 ? void 0 : qrResult.paint_text) ||
767
954
  (qrResult === null || qrResult === void 0 ? void 0 : qrResult.raw_qr_code) ||
768
955
  (qrResult === null || qrResult === void 0 ? void 0 : qrResult.qr_code) ||
@@ -770,8 +957,14 @@ class ThaiTicketMajor {
770
957
  ((_c = qrResult === null || qrResult === void 0 ? void 0 : qrResult.data) === null || _c === void 0 ? void 0 : _c.qr_code) ||
771
958
  ((_d = qrResult === null || qrResult === void 0 ? void 0 : qrResult.data) === null || _d === void 0 ? void 0 : _d.raw_qr_code) ||
772
959
  (typeof qrResult === "string" ? qrResult : "");
960
+ const qrAmount = (0, exports.parseThaiQRAmount)(qrRawCode);
961
+ const finalAmount = qrAmount ||
962
+ (targetAmount && targetAmount !== "0" ? targetAmount : null) ||
963
+ (qrResult === null || qrResult === void 0 ? void 0 : qrResult.amount) ||
964
+ ((_e = qrResult === null || qrResult === void 0 ? void 0 : qrResult.data) === null || _e === void 0 ? void 0 : _e.amount) ||
965
+ "0";
773
966
  const displayQR = (customTitle) => {
774
- const title = customTitle || `ThaiQR Payment (KBank) - ยอดชำระ: ${orderEncData.amount || paymentAllData["cal_totalamount"]} บาท`;
967
+ const title = customTitle || `ThaiQR Payment (KBank) - ยอดชำระ: ${finalAmount} บาท`;
775
968
  ThaiTicketMajor.displayQRCode(qrRawCode, title);
776
969
  };
777
970
  if (options.printQR !== false && qrRawCode) {
@@ -780,7 +973,7 @@ class ThaiTicketMajor {
780
973
  return {
781
974
  success: true,
782
975
  orderId: kbankAuth.orderId,
783
- amount: orderEncData.amount || paymentAllData["cal_totalamount"],
976
+ amount: String(finalAmount),
784
977
  qrRawCode,
785
978
  qrData: qrResult,
786
979
  displayQR,
@@ -791,7 +984,7 @@ class ThaiTicketMajor {
791
984
  venue: paymentAllData["venue"],
792
985
  zone: targetZone.value,
793
986
  ticketCount,
794
- totalAmount: orderEncData.amount || paymentAllData["cal_totalamount"],
987
+ totalAmount: String(finalAmount),
795
988
  seats: summarySeats.length > 0 ? summarySeats : undefined,
796
989
  },
797
990
  rawPayload: {
@@ -823,5 +1016,24 @@ class ThaiTicketMajor {
823
1016
  }
824
1017
  }
825
1018
  exports.ThaiTicketMajor = ThaiTicketMajor;
1019
+ ThaiTicketMajor.BOT_PROTECTION_COOKIES = new Set([
1020
+ "_abck",
1021
+ "bm_sz",
1022
+ "ak_bmsc",
1023
+ "bm_sv",
1024
+ "bm_mi",
1025
+ "hwwafsesid",
1026
+ "hwwafsestime",
1027
+ "__cf_bm",
1028
+ "cf_clearance",
1029
+ "_ga",
1030
+ "_gid",
1031
+ "_gcl_au",
1032
+ "_fbp",
1033
+ "_clck",
1034
+ "_clsk",
1035
+ "_ttp",
1036
+ "_twpid",
1037
+ ]);
826
1038
  exports.displayQRCode = ThaiTicketMajor.displayQRCode;
827
1039
  exports.default = ThaiTicketMajor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playttm",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Make HTML Metadata to JSON",
5
5
  "main": "dist/lib.js",
6
6
  "types": "dist/lib.d.ts",
package/src/lib.ts CHANGED
@@ -3,6 +3,37 @@ import * as cheerio from "cheerio";
3
3
  import { wrapper } from "axios-cookiejar-support";
4
4
  import { CookieJar } from "tough-cookie";
5
5
 
6
+ declare const chrome: any;
7
+
8
+ export const isBrowserEnvironment = (): boolean => {
9
+ if (typeof window !== "undefined" && typeof window.document !== "undefined") {
10
+ return true;
11
+ }
12
+ if (typeof globalThis !== "undefined" && Boolean((globalThis as any).chrome?.runtime?.id)) {
13
+ return true;
14
+ }
15
+ if (typeof process === "undefined" || !process.versions || !process.versions.node) {
16
+ return true;
17
+ }
18
+ return false;
19
+ };
20
+
21
+ /**
22
+ * Extract transaction amount from EMVCo / ThaiQR payload (Tag 54)
23
+ */
24
+ export const parseThaiQRAmount = (qrCode?: string): string | null => {
25
+ if (!qrCode) return null;
26
+ const match = qrCode.match(/(?:^|[^0-9])54(\d{2})([0-9.]+)/);
27
+ if (match) {
28
+ const len = parseInt(match[1], 10);
29
+ const amountStr = match[2].substring(0, len);
30
+ if (!isNaN(parseFloat(amountStr))) {
31
+ return amountStr;
32
+ }
33
+ }
34
+ return null;
35
+ };
36
+
6
37
  // ==========================================
7
38
  // Interfaces & Types
8
39
  // ==========================================
@@ -14,6 +45,8 @@ export interface TTMConfig {
14
45
  timeout?: number;
15
46
  endpoint?: string;
16
47
  onProgress?: (step: string, message: string, data?: any) => void;
48
+ isBrowser?: boolean;
49
+ adapter?: any;
17
50
  }
18
51
 
19
52
  export interface RoundInfo {
@@ -91,13 +124,19 @@ export interface QuickBookResult {
91
124
 
92
125
  export class ThaiTicketMajor {
93
126
  private client: AxiosInstance;
94
- private jar: CookieJar;
127
+ private jar?: CookieJar;
95
128
  private endpoint: string;
96
129
  private defaultHeaders: Record<string, string>;
97
130
  private onProgressCallback?: (step: string, message: string, data?: any) => void;
131
+ public inBrowser: boolean;
132
+ private rawCookies?: string;
98
133
 
99
134
  constructor(config: TTMConfig = {}) {
100
- this.jar = new CookieJar();
135
+ this.inBrowser =
136
+ typeof config.isBrowser === "boolean"
137
+ ? config.isBrowser
138
+ : isBrowserEnvironment();
139
+ this.rawCookies = config.cookies;
101
140
  this.endpoint = config.endpoint || "https://booking.thaiticketmajor.com/booking/3m";
102
141
  this.onProgressCallback = config.onProgress;
103
142
 
@@ -108,22 +147,180 @@ export class ThaiTicketMajor {
108
147
  this.defaultHeaders = {
109
148
  accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
110
149
  "accept-language": "th,en-US;q=0.9,en;q=0.8",
111
- "user-agent": defaultUA,
112
- referer: "https://booking.thaiticketmajor.com",
113
150
  ...(config.headers || {}),
114
151
  };
115
152
 
116
- if (config.cookies) {
117
- this.defaultHeaders["cookie"] = config.cookies;
118
- }
153
+ if (!this.inBrowser) {
154
+ this.jar = new CookieJar();
155
+ this.defaultHeaders["user-agent"] = defaultUA;
156
+ this.defaultHeaders["referer"] = "https://booking.thaiticketmajor.com";
157
+ if (config.cookies) {
158
+ this.defaultHeaders["cookie"] = config.cookies;
159
+ }
119
160
 
120
- this.client = wrapper(
121
- axios.create({
122
- jar: this.jar,
161
+ this.client = wrapper(
162
+ axios.create({
163
+ jar: this.jar,
164
+ withCredentials: true,
165
+ timeout: config.timeout || 30000,
166
+ ...(config.adapter ? { adapter: config.adapter } : {}),
167
+ } as any) as any
168
+ ) as any;
169
+ } else {
170
+ // Browser / Chrome Extension environment
171
+ this.client = axios.create({
123
172
  withCredentials: true,
124
173
  timeout: config.timeout || 30000,
125
- } as any) as any
126
- ) as any;
174
+ ...(config.adapter ? { adapter: config.adapter } : {}),
175
+ });
176
+
177
+ // Interceptor to strip unsafe headers forbidden by browsers (Fetch / XHR specification)
178
+ const unsafeHeaders = ["user-agent", "referer", "cookie", "origin", "host", "connection"];
179
+ this.client.interceptors.request.use((reqConfig) => {
180
+ if (reqConfig.headers) {
181
+ for (const header of unsafeHeaders) {
182
+ delete (reqConfig.headers as any)[header];
183
+ delete (reqConfig.headers as any)[header.toLowerCase()];
184
+ delete (reqConfig.headers as any)[header.toUpperCase()];
185
+ const titleCase = header.charAt(0).toUpperCase() + header.slice(1);
186
+ delete (reqConfig.headers as any)[titleCase];
187
+ }
188
+ }
189
+ return reqConfig;
190
+ });
191
+ }
192
+ }
193
+
194
+ public static readonly BOT_PROTECTION_COOKIES = new Set([
195
+ "_abck",
196
+ "bm_sz",
197
+ "ak_bmsc",
198
+ "bm_sv",
199
+ "bm_mi",
200
+ "hwwafsesid",
201
+ "hwwafsestime",
202
+ "__cf_bm",
203
+ "cf_clearance",
204
+ "_ga",
205
+ "_gid",
206
+ "_gcl_au",
207
+ "_fbp",
208
+ "_clck",
209
+ "_clsk",
210
+ "_ttp",
211
+ "_twpid",
212
+ ]);
213
+
214
+ /**
215
+ * Synchronize a cookie string into Chrome Extension's cookie store (chrome.cookies API).
216
+ * Automatically ignores bot protection cookies (Akamai _abck, bm_*, WAF) to prevent Access Denied (403) bans.
217
+ */
218
+ public static async syncCookiesToChrome(cookieString: string, domainUrl: string = "https://booking.thaiticketmajor.com"): Promise<void> {
219
+ if (typeof chrome === "undefined" || !chrome?.cookies?.set) {
220
+ return;
221
+ }
222
+
223
+ const pairs = cookieString.split(";");
224
+ for (const pair of pairs) {
225
+ const trimmed = pair.trim();
226
+ if (!trimmed) continue;
227
+ const eqIndex = trimmed.indexOf("=");
228
+ if (eqIndex === -1) continue;
229
+
230
+ const name = trimmed.substring(0, eqIndex).trim();
231
+ const value = trimmed.substring(eqIndex + 1).trim();
232
+ if (!name) continue;
233
+
234
+ // DO NOT overwrite Akamai / WAF bot tokens!
235
+ // Writing stale _abck or bm_* triggers Akamai Bot Protection -> Access Denied.
236
+ if (ThaiTicketMajor.BOT_PROTECTION_COOKIES.has(name.toLowerCase())) {
237
+ continue;
238
+ }
239
+
240
+ try {
241
+ await chrome.cookies.set({
242
+ url: domainUrl,
243
+ name,
244
+ value,
245
+ domain: ".thaiticketmajor.com",
246
+ path: "/",
247
+ });
248
+ } catch {
249
+ try {
250
+ await chrome.cookies.set({
251
+ url: domainUrl,
252
+ name,
253
+ value,
254
+ path: "/",
255
+ });
256
+ } catch {}
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Setup declarativeNetRequest session rules in Chrome Extension to set Referer header for API requests only.
263
+ */
264
+ public static async setupExtensionRules(): Promise<void> {
265
+ if (typeof chrome === "undefined" || !chrome?.declarativeNetRequest?.updateSessionRules) {
266
+ return;
267
+ }
268
+
269
+ try {
270
+ await chrome.declarativeNetRequest.updateSessionRules({
271
+ removeRuleIds: [1001, 1002],
272
+ addRules: [
273
+ {
274
+ id: 1001,
275
+ priority: 1,
276
+ action: {
277
+ type: chrome.declarativeNetRequest.RuleActionType?.MODIFY_HEADERS || "modifyHeaders",
278
+ requestHeaders: [
279
+ {
280
+ header: "Referer",
281
+ operation: chrome.declarativeNetRequest.HeaderOperation?.SET || "set",
282
+ value: "https://booking.thaiticketmajor.com",
283
+ },
284
+ ],
285
+ },
286
+ condition: {
287
+ urlFilter: "thaiticketmajor.com",
288
+ resourceTypes: [
289
+ chrome.declarativeNetRequest.ResourceType?.XMLHTTPREQUEST || "xmlhttprequest",
290
+ ],
291
+ },
292
+ },
293
+ {
294
+ id: 1002,
295
+ priority: 1,
296
+ action: {
297
+ type: chrome.declarativeNetRequest.RuleActionType?.MODIFY_HEADERS || "modifyHeaders",
298
+ requestHeaders: [
299
+ {
300
+ header: "Referer",
301
+ operation: chrome.declarativeNetRequest.HeaderOperation?.SET || "set",
302
+ value: "https://kpaymentgateway.kasikornbank.com/",
303
+ },
304
+ ],
305
+ },
306
+ condition: {
307
+ urlFilter: "kasikornbank.com",
308
+ resourceTypes: [
309
+ chrome.declarativeNetRequest.ResourceType?.XMLHTTPREQUEST || "xmlhttprequest",
310
+ ],
311
+ },
312
+ },
313
+ ],
314
+ });
315
+ } catch (err) {
316
+ console.warn("Could not set declarativeNetRequest session rules:", err);
317
+ }
318
+ }
319
+
320
+ public async syncCookies(domainUrl?: string): Promise<void> {
321
+ if (this.rawCookies) {
322
+ await ThaiTicketMajor.syncCookiesToChrome(this.rawCookies, domainUrl);
323
+ }
127
324
  }
128
325
 
129
326
  private emitProgress(step: string, message: string, data?: any) {
@@ -928,6 +1125,15 @@ export class ThaiTicketMajor {
928
1125
  this.onProgressCallback = options.onProgress;
929
1126
  }
930
1127
 
1128
+ // In Chrome Extension environment, prepare cookies and network rules
1129
+ if (this.inBrowser) {
1130
+ this.emitProgress("INIT_EXTENSION", "Preparing Chrome extension cookies and network rules...");
1131
+ if (this.rawCookies) {
1132
+ await this.syncCookies();
1133
+ }
1134
+ await ThaiTicketMajor.setupExtensionRules();
1135
+ }
1136
+
931
1137
  // 1. ดึงรอบการแสดง
932
1138
  const rounds = await this.getRounds(eventUrl);
933
1139
  if (rounds.length === 0) {
@@ -1062,7 +1268,16 @@ export class ThaiTicketMajor {
1062
1268
  const kbankInfo = await this.getKBankQR(orderEncData);
1063
1269
  const kbankAuth = await this.paymentKBankQR(orderEncData, kbankInfo);
1064
1270
 
1065
- const qrResult = await this.generateThaiQR(kbankAuth.orderId, kbankAuth.apiKey, orderEncData.amount);
1271
+ const targetAmount =
1272
+ orderEncData.transactionamount ||
1273
+ orderEncData.totalamount ||
1274
+ orderEncData.amount ||
1275
+ payCfmData.amount ||
1276
+ kbankInfo.totalamount ||
1277
+ paymentAllData["cal_totalamount"] ||
1278
+ "0";
1279
+
1280
+ const qrResult = await this.generateThaiQR(kbankAuth.orderId, kbankAuth.apiKey, targetAmount);
1066
1281
 
1067
1282
  const qrRawCode =
1068
1283
  qrResult?.paint_text ||
@@ -1073,8 +1288,16 @@ export class ThaiTicketMajor {
1073
1288
  qrResult?.data?.raw_qr_code ||
1074
1289
  (typeof qrResult === "string" ? qrResult : "");
1075
1290
 
1291
+ const qrAmount = parseThaiQRAmount(qrRawCode);
1292
+ const finalAmount =
1293
+ qrAmount ||
1294
+ (targetAmount && targetAmount !== "0" ? targetAmount : null) ||
1295
+ qrResult?.amount ||
1296
+ qrResult?.data?.amount ||
1297
+ "0";
1298
+
1076
1299
  const displayQR = (customTitle?: string) => {
1077
- const title = customTitle || `ThaiQR Payment (KBank) - ยอดชำระ: ${orderEncData.amount || paymentAllData["cal_totalamount"]} บาท`;
1300
+ const title = customTitle || `ThaiQR Payment (KBank) - ยอดชำระ: ${finalAmount} บาท`;
1078
1301
  ThaiTicketMajor.displayQRCode(qrRawCode, title);
1079
1302
  };
1080
1303
 
@@ -1085,7 +1308,7 @@ export class ThaiTicketMajor {
1085
1308
  return {
1086
1309
  success: true,
1087
1310
  orderId: kbankAuth.orderId,
1088
- amount: orderEncData.amount || paymentAllData["cal_totalamount"],
1311
+ amount: String(finalAmount),
1089
1312
  qrRawCode,
1090
1313
  qrData: qrResult,
1091
1314
  displayQR,
@@ -1096,7 +1319,7 @@ export class ThaiTicketMajor {
1096
1319
  venue: paymentAllData["venue"],
1097
1320
  zone: targetZone.value,
1098
1321
  ticketCount,
1099
- totalAmount: orderEncData.amount || paymentAllData["cal_totalamount"],
1322
+ totalAmount: String(finalAmount),
1100
1323
  seats: summarySeats.length > 0 ? summarySeats : undefined,
1101
1324
  },
1102
1325
  rawPayload: {