iframe-tracking-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ import { TrackingEvent, LearningEventPayload } from './types';
2
+ /**
3
+ * Category của từng event để backend routing/classification
4
+ */
5
+ export type EventCategory = 'lifecycle' | 'vocabulary' | 'question' | 'media' | 'speaking' | 'system' | 'unknown';
6
+ /**
7
+ * Cấu trúc request body chuẩn gửi lên API batch endpoint.
8
+ * session_id được cố ý loại bỏ (chỉ dùng local debug).
9
+ */
10
+ export interface BatchRequestBody {
11
+ user_id: string;
12
+ app_id: string;
13
+ app_version: string;
14
+ events: Array<{
15
+ event_id: string;
16
+ event_name: string;
17
+ timestamp: number;
18
+ client_timestamp: number;
19
+ [key: string]: any;
20
+ }>;
21
+ }
22
+ /**
23
+ * EventProcessor - Pure business logic, không phụ thuộc browser API.
24
+ * Có thể sử dụng ở cả frontend (React/Vite) lẫn backend (NestJS/Node.js).
25
+ *
26
+ * @example Backend NestJS usage:
27
+ * ```typescript
28
+ * import { EventProcessor } from '@bkt/iframe-tracking-sdk/processor';
29
+ *
30
+ * // Nhận raw events từ request body
31
+ * const normalized = rawEvents.map(e => EventProcessor.normalize(e)).filter(Boolean);
32
+ * const batch = EventProcessor.buildBatchPayload(normalized);
33
+ * const categorized = EventProcessor.groupByCategory(normalized);
34
+ * ```
35
+ */
36
+ export declare class EventProcessor {
37
+ private static readonly LIFECYCLE_EVENTS;
38
+ private static readonly VOCABULARY_EVENTS;
39
+ private static readonly QUESTION_EVENTS;
40
+ private static readonly MEDIA_EVENTS;
41
+ private static readonly SPEAKING_EVENTS;
42
+ private static readonly SYSTEM_EVENTS;
43
+ /**
44
+ * Normalize và validate một raw event object thành TrackingEvent đầy đủ.
45
+ * Trả về `null` nếu event thiếu `event_name` (invalid).
46
+ */
47
+ static normalize(raw: Partial<TrackingEvent> & {
48
+ event_name?: string;
49
+ }, defaults?: {
50
+ user_id?: string;
51
+ app_id?: string;
52
+ }): TrackingEvent | null;
53
+ /**
54
+ * Build batch request body theo chuẩn API `/api/progress/batch`.
55
+ * `session_id` được cố ý loại bỏ (không gửi lên server).
56
+ */
57
+ static buildBatchPayload(events: TrackingEvent[], appVersion?: string): BatchRequestBody;
58
+ /**
59
+ * Phân loại (classify) một sự kiện theo category.
60
+ */
61
+ static classify(eventName: string): EventCategory;
62
+ /**
63
+ * Nhóm các events theo category - hữu ích cho backend routing.
64
+ */
65
+ static groupByCategory(events: TrackingEvent[]): Record<EventCategory, TrackingEvent[]>;
66
+ /**
67
+ * Kiểm tra xem event có phải là "critical" không
68
+ * (cần immediate sync, không chờ batch interval).
69
+ */
70
+ static isCritical(eventName: string): boolean;
71
+ /**
72
+ * Tính điểm đúng/sai từ payload event.
73
+ * Trả về null nếu event không có thông tin điểm số.
74
+ */
75
+ static extractScore(event: TrackingEvent): {
76
+ isCorrect: boolean | null;
77
+ score: number | null;
78
+ };
79
+ /**
80
+ * Parse raw postMessage data từ Iframe thành LearningEventPayload.
81
+ * Dùng cho trường hợp backend cần xử lý message trực tiếp (WebSocket, etc.).
82
+ */
83
+ static parseIframeMessage(rawData: unknown): LearningEventPayload | null;
84
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * EventProcessor - Pure business logic, không phụ thuộc browser API.
3
+ * Có thể sử dụng ở cả frontend (React/Vite) lẫn backend (NestJS/Node.js).
4
+ *
5
+ * @example Backend NestJS usage:
6
+ * ```typescript
7
+ * import { EventProcessor } from '@bkt/iframe-tracking-sdk/processor';
8
+ *
9
+ * // Nhận raw events từ request body
10
+ * const normalized = rawEvents.map(e => EventProcessor.normalize(e)).filter(Boolean);
11
+ * const batch = EventProcessor.buildBatchPayload(normalized);
12
+ * const categorized = EventProcessor.groupByCategory(normalized);
13
+ * ```
14
+ */
15
+ export class EventProcessor {
16
+ static LIFECYCLE_EVENTS = new Set([
17
+ 'session_start', 'session_end',
18
+ 'course_start', 'course_finish',
19
+ 'unit_start', 'unit_finish', 'unit_submit', 'unit_restart'
20
+ ]);
21
+ static VOCABULARY_EVENTS = new Set([
22
+ 'word_click', 'word_view', 'word_answer',
23
+ 'flashcard_flip', 'flashcard_know'
24
+ ]);
25
+ static QUESTION_EVENTS = new Set([
26
+ 'question_view', 'question_answer',
27
+ 'drag_drop_interaction', 'text_input_change',
28
+ 'show_solution', 'question_next', 'question_prev',
29
+ 'image_view'
30
+ ]);
31
+ static MEDIA_EVENTS = new Set([
32
+ 'audio_play', 'audio_pause', 'audio_completed',
33
+ 'video_play', 'video_pause', 'video_seek', 'video_completed'
34
+ ]);
35
+ static SPEAKING_EVENTS = new Set([
36
+ 'pronunciation_record', 'pronunciation_score'
37
+ ]);
38
+ static SYSTEM_EVENTS = new Set([
39
+ 'client_error', 'network_latency_check'
40
+ ]);
41
+ /**
42
+ * Normalize và validate một raw event object thành TrackingEvent đầy đủ.
43
+ * Trả về `null` nếu event thiếu `event_name` (invalid).
44
+ */
45
+ static normalize(raw, defaults) {
46
+ const eventName = raw.event_name || raw.payload?.event_name;
47
+ if (!eventName)
48
+ return null;
49
+ const now = Date.now();
50
+ const randomChars = Math.random().toString(36).substring(2, 10);
51
+ return {
52
+ event_id: raw.event_id || `evt_${now}_${randomChars}`,
53
+ user_id: raw.user_id || defaults?.user_id || '',
54
+ app_id: raw.app_id || defaults?.app_id || '',
55
+ event_name: eventName,
56
+ payload: raw.payload || { event_name: eventName },
57
+ timestamp: raw.timestamp || now,
58
+ client_timestamp: raw.client_timestamp || now,
59
+ status: 'pending',
60
+ retry_count: raw.retry_count || 0,
61
+ created_at: raw.created_at || now,
62
+ ...(raw.session_id ? { session_id: raw.session_id } : {}),
63
+ ...(raw.signature ? { signature: raw.signature } : {}),
64
+ };
65
+ }
66
+ /**
67
+ * Build batch request body theo chuẩn API `/api/progress/batch`.
68
+ * `session_id` được cố ý loại bỏ (không gửi lên server).
69
+ */
70
+ static buildBatchPayload(events, appVersion = '1.0.0') {
71
+ if (events.length === 0)
72
+ throw new Error('[EventProcessor] Cannot build batch from empty events array');
73
+ return {
74
+ user_id: events[0].user_id,
75
+ app_id: events[0].app_id,
76
+ app_version: appVersion,
77
+ events: events.map(e => {
78
+ // Flatten payload fields vào event, loại bỏ event_name trùng lặp
79
+ const { event_name: _en, ...payloadRest } = e.payload;
80
+ return {
81
+ event_id: e.event_id,
82
+ event_name: e.event_name,
83
+ timestamp: e.timestamp,
84
+ client_timestamp: e.client_timestamp,
85
+ ...payloadRest, // word_id, score, is_correct, duration_ms, ...
86
+ };
87
+ })
88
+ };
89
+ }
90
+ /**
91
+ * Phân loại (classify) một sự kiện theo category.
92
+ */
93
+ static classify(eventName) {
94
+ if (this.LIFECYCLE_EVENTS.has(eventName))
95
+ return 'lifecycle';
96
+ if (this.VOCABULARY_EVENTS.has(eventName))
97
+ return 'vocabulary';
98
+ if (this.QUESTION_EVENTS.has(eventName))
99
+ return 'question';
100
+ if (this.MEDIA_EVENTS.has(eventName))
101
+ return 'media';
102
+ if (this.SPEAKING_EVENTS.has(eventName))
103
+ return 'speaking';
104
+ if (this.SYSTEM_EVENTS.has(eventName))
105
+ return 'system';
106
+ return 'unknown';
107
+ }
108
+ /**
109
+ * Nhóm các events theo category - hữu ích cho backend routing.
110
+ */
111
+ static groupByCategory(events) {
112
+ const groups = {
113
+ lifecycle: [],
114
+ vocabulary: [],
115
+ question: [],
116
+ media: [],
117
+ speaking: [],
118
+ system: [],
119
+ unknown: []
120
+ };
121
+ for (const event of events) {
122
+ const cat = this.classify(event.event_name);
123
+ groups[cat].push(event);
124
+ }
125
+ return groups;
126
+ }
127
+ /**
128
+ * Kiểm tra xem event có phải là "critical" không
129
+ * (cần immediate sync, không chờ batch interval).
130
+ */
131
+ static isCritical(eventName) {
132
+ const criticalEvents = new Set([
133
+ 'session_end',
134
+ 'course_finish',
135
+ 'unit_finish',
136
+ 'unit_submit',
137
+ 'unit_restart',
138
+ 'pronunciation_score',
139
+ 'client_error',
140
+ ]);
141
+ return criticalEvents.has(eventName);
142
+ }
143
+ /**
144
+ * Tính điểm đúng/sai từ payload event.
145
+ * Trả về null nếu event không có thông tin điểm số.
146
+ */
147
+ static extractScore(event) {
148
+ const p = event.payload;
149
+ // is_correct field trực tiếp
150
+ if (typeof p.is_correct === 'boolean') {
151
+ return { isCorrect: p.is_correct, score: p.score ?? null };
152
+ }
153
+ // result field: "correct" | "incorrect"
154
+ if (p.result === 'correct')
155
+ return { isCorrect: true, score: p.score ?? null };
156
+ if (p.result === 'incorrect')
157
+ return { isCorrect: false, score: p.score ?? null };
158
+ // pronunciation_score: overall_score >= 80 là đúng
159
+ if (event.event_name === 'pronunciation_score' && typeof p.overall_score === 'number') {
160
+ return { isCorrect: p.overall_score >= 80, score: p.overall_score };
161
+ }
162
+ return { isCorrect: null, score: p.score ?? p.overall_score ?? null };
163
+ }
164
+ /**
165
+ * Parse raw postMessage data từ Iframe thành LearningEventPayload.
166
+ * Dùng cho trường hợp backend cần xử lý message trực tiếp (WebSocket, etc.).
167
+ */
168
+ static parseIframeMessage(rawData) {
169
+ if (!rawData || typeof rawData !== 'object')
170
+ return null;
171
+ const data = rawData;
172
+ if (data['type'] !== 'LEARNING_EVENT')
173
+ return null;
174
+ const payload = data['payload'];
175
+ if (!payload || typeof payload !== 'object' || !payload['event_name'])
176
+ return null;
177
+ return payload;
178
+ }
179
+ }
@@ -0,0 +1,21 @@
1
+ import { RefObject } from 'react';
2
+ import { IframeHostTrackerConfig, TrackingEvent } from './types';
3
+ export interface UseIframeHostTrackingResult {
4
+ iframeRef: RefObject<HTMLIFrameElement | null>;
5
+ isReady: boolean;
6
+ syncing: boolean;
7
+ events: TrackingEvent[];
8
+ flush: () => Promise<void>;
9
+ handleIframeLoad: () => void;
10
+ clearLogs: () => void;
11
+ clearQueue: () => Promise<void>;
12
+ /** Update optional local sessionId + hmacSecret for backward-compatible hosts */
13
+ updateCredentials: (sessionId: string, hmacSecret: string) => void;
14
+ /** Re-initiate handshake with the iframe */
15
+ reinitiateHandshake: () => void;
16
+ onEventReceived?: (event: TrackingEvent) => void;
17
+ }
18
+ export declare function useIframeHostTracking(config: Omit<IframeHostTrackerConfig, 'onEventReceived' | 'onEventSynced' | 'onSyncStatusChange'> & {
19
+ onEventReceived?: (event: TrackingEvent) => void;
20
+ onSessionRefreshNeeded?: (eventName: string) => void;
21
+ }): UseIframeHostTrackingResult;
@@ -0,0 +1,142 @@
1
+ import { useEffect, useRef, useState, useCallback } from 'react';
2
+ import { IframeHostTracker } from './host';
3
+ export function useIframeHostTracking(config) {
4
+ const iframeRef = useRef(null);
5
+ const trackerRef = useRef(null);
6
+ const [isReady, setIsReady] = useState(false);
7
+ const [syncing, setSyncing] = useState(false);
8
+ const [events, setEvents] = useState([]);
9
+ // Dùng ref để tránh stale closure — luôn gọi phiên bản callback mới nhất
10
+ const onEventReceivedRef = useRef(config.onEventReceived);
11
+ useEffect(() => {
12
+ onEventReceivedRef.current = config.onEventReceived;
13
+ }, [config.onEventReceived]);
14
+ // Refresh logs from database
15
+ const refreshLogs = useCallback(async () => {
16
+ if (!trackerRef.current)
17
+ return;
18
+ try {
19
+ const db = trackerRef.current.getDB();
20
+ const allEvents = await db.getAllEvents(100);
21
+ setEvents(allEvents);
22
+ }
23
+ catch (e) {
24
+ if (config.debug)
25
+ console.error('[Tracking Hook] Failed to load events from DB:', e);
26
+ }
27
+ }, [config.debug]);
28
+ // Handle iframe onload
29
+ const handleIframeLoad = useCallback(() => {
30
+ const iframe = iframeRef.current;
31
+ if (iframe && iframe.contentWindow && trackerRef.current) {
32
+ if (config.debug)
33
+ console.log('[Tracking Hook] Iframe loaded. Initiating secure handshake...');
34
+ trackerRef.current.initiateHandshake(iframe.contentWindow);
35
+ }
36
+ }, [config.debug]);
37
+ // Flush events
38
+ const flush = useCallback(async () => {
39
+ if (trackerRef.current) {
40
+ await trackerRef.current.flush();
41
+ await refreshLogs();
42
+ }
43
+ }, [refreshLogs]);
44
+ // Clear UI Logs
45
+ const clearLogs = useCallback(() => {
46
+ setEvents([]);
47
+ }, []);
48
+ // Clear local IndexedDB queue
49
+ const clearQueue = useCallback(async () => {
50
+ if (trackerRef.current) {
51
+ await trackerRef.current.clearQueue();
52
+ setEvents([]);
53
+ }
54
+ }, []);
55
+ // Update optional local sessionId + hmacSecret on the underlying tracker instance
56
+ const updateCredentials = useCallback((sessionId, hmacSecret) => {
57
+ if (trackerRef.current) {
58
+ trackerRef.current.updateCredentials(sessionId, hmacSecret);
59
+ }
60
+ }, []);
61
+ // Re-initiate handshake with the iframe (e.g., after credentials refresh)
62
+ const reinitiateHandshake = useCallback(() => {
63
+ const iframe = iframeRef.current;
64
+ if (iframe && iframe.contentWindow && trackerRef.current) {
65
+ if (config.debug)
66
+ console.log('[Tracking Hook] Re-initiating handshake after credential refresh...');
67
+ trackerRef.current.initiateHandshake(iframe.contentWindow);
68
+ }
69
+ }, [config.debug]);
70
+ // Initialize tracker on mount, recreate if dynamic credentials change
71
+ useEffect(() => {
72
+ if (typeof window === 'undefined')
73
+ return;
74
+ if (config.debug)
75
+ console.log('[Tracking Hook] Initializing IframeHostTracker...');
76
+ const tracker = new IframeHostTracker({
77
+ ...config,
78
+ onSessionRefreshNeeded: config.onSessionRefreshNeeded,
79
+ onEventReceived: (event) => {
80
+ if (config.debug)
81
+ console.log('[Tracking Hook] Event received:', event.event_name);
82
+ // Gọi qua ref để luôn dùng callback mới nhất (tránh stale closure)
83
+ onEventReceivedRef.current?.(event);
84
+ refreshLogs();
85
+ },
86
+ onEventSynced: (ids) => {
87
+ if (config.debug)
88
+ console.log('[Tracking Hook] Events synced:', ids);
89
+ refreshLogs();
90
+ },
91
+ onSyncStatusChange: (status) => {
92
+ setSyncing(status === 'syncing');
93
+ }
94
+ });
95
+ trackerRef.current = tracker;
96
+ tracker.start();
97
+ // Load initial events from IndexedDB
98
+ refreshLogs();
99
+ // Check if iframe is already loaded
100
+ const iframe = iframeRef.current;
101
+ if (iframe && iframe.contentWindow) {
102
+ tracker.initiateHandshake(iframe.contentWindow);
103
+ }
104
+ // Set up handshake completion checker
105
+ const checkHandshakeInterval = setInterval(() => {
106
+ // Accessing private variable safely via class status
107
+ if (tracker && tracker.isHandshakeComplete) {
108
+ setIsReady(true);
109
+ }
110
+ else {
111
+ setIsReady(false);
112
+ }
113
+ }, 1000);
114
+ return () => {
115
+ clearInterval(checkHandshakeInterval);
116
+ tracker.stop();
117
+ trackerRef.current = null;
118
+ };
119
+ }, [
120
+ config.iframeUrl,
121
+ config.userId,
122
+ config.sessionId,
123
+ config.hmacSecret,
124
+ config.apiEndpoint,
125
+ config.appId,
126
+ config.sectionId,
127
+ config.trustedOrigins.join(','),
128
+ refreshLogs
129
+ ]);
130
+ return {
131
+ iframeRef,
132
+ isReady,
133
+ syncing,
134
+ events,
135
+ flush,
136
+ handleIframeLoad,
137
+ clearLogs,
138
+ clearQueue,
139
+ updateCredentials,
140
+ reinitiateHandshake,
141
+ };
142
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Cryptographic security and verification helpers.
3
+ */
4
+ /**
5
+ * Generate a cryptographically secure UUID-v4.
6
+ */
7
+ export declare function generateNonce(): string;
8
+ /**
9
+ * Sort object keys alphabetically recursively to maintain signature consistency.
10
+ */
11
+ export declare function sortObjectKeys(obj: any): any;
12
+ /**
13
+ * Generate a canonical string representation for an event to be signed.
14
+ * canonical_string = event_name + "|" + event_id + "|" + timestamp + "|" + JSON.stringify(payload_sorted)
15
+ */
16
+ export declare function buildCanonicalString(eventName: string, eventId: string, timestamp: number, payload: any): string;
17
+ /**
18
+ * Generate browser-native HMAC-SHA256 signature in Hex format.
19
+ */
20
+ export declare function signHmacSha256(secret: string, canonicalString: string): Promise<string>;
21
+ /**
22
+ * Verify browser-native HMAC-SHA256 signature.
23
+ */
24
+ export declare function verifyHmacSha256(secret: string, canonicalString: string, signatureHex: string): Promise<boolean>;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Cryptographic security and verification helpers.
3
+ */
4
+ /**
5
+ * Generate a cryptographically secure UUID-v4.
6
+ */
7
+ export function generateNonce() {
8
+ if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
9
+ return window.crypto.randomUUID();
10
+ }
11
+ // Fallback if randomUUID is not available (e.g. in some older environments)
12
+ const array = new Uint32Array(4);
13
+ if (typeof window !== 'undefined' && window.crypto) {
14
+ window.crypto.getRandomValues(array);
15
+ }
16
+ else {
17
+ // Basic Node/SSR fallback
18
+ for (let i = 0; i < 4; i++) {
19
+ array[i] = Math.floor(Math.random() * 0xffffffff);
20
+ }
21
+ }
22
+ let hex = '';
23
+ for (let i = 0; i < 4; i++) {
24
+ hex += array[i].toString(16).padStart(8, '0');
25
+ }
26
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
27
+ }
28
+ /**
29
+ * Sort object keys alphabetically recursively to maintain signature consistency.
30
+ */
31
+ export function sortObjectKeys(obj) {
32
+ if (typeof obj !== 'object' || obj === null) {
33
+ return obj;
34
+ }
35
+ if (Array.isArray(obj)) {
36
+ return obj.map(sortObjectKeys);
37
+ }
38
+ const sorted = {};
39
+ const keys = Object.keys(obj).sort();
40
+ for (const key of keys) {
41
+ sorted[key] = sortObjectKeys(obj[key]);
42
+ }
43
+ return sorted;
44
+ }
45
+ /**
46
+ * Generate a canonical string representation for an event to be signed.
47
+ * canonical_string = event_name + "|" + event_id + "|" + timestamp + "|" + JSON.stringify(payload_sorted)
48
+ */
49
+ export function buildCanonicalString(eventName, eventId, timestamp, payload) {
50
+ const sortedPayload = sortObjectKeys(payload);
51
+ return `${eventName}|${eventId}|${timestamp}|${JSON.stringify(sortedPayload)}`;
52
+ }
53
+ /**
54
+ * Generate browser-native HMAC-SHA256 signature in Hex format.
55
+ */
56
+ export async function signHmacSha256(secret, canonicalString) {
57
+ // Use browser-native Web Crypto API if available (faster & built-in)
58
+ if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) {
59
+ try {
60
+ const encoder = new TextEncoder();
61
+ const keyData = encoder.encode(secret);
62
+ const messageData = encoder.encode(canonicalString);
63
+ const cryptoKey = await window.crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
64
+ const signatureBuffer = await window.crypto.subtle.sign('HMAC', cryptoKey, messageData);
65
+ const hashArray = Array.from(new Uint8Array(signatureBuffer));
66
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
67
+ }
68
+ catch (e) {
69
+ // Fall back to pure JS if Web Crypto fails unexpectedly
70
+ }
71
+ }
72
+ // Fallback: Pure JS HMAC-SHA256 (for insecure contexts like HTTP on IP)
73
+ const signatureBuffer = hmacSha256Pure(secret, canonicalString);
74
+ const hashArray = Array.from(signatureBuffer);
75
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
76
+ }
77
+ // Pure JS SHA-256 and HMAC-SHA256 helper functions
78
+ function sha256Pure(bytes) {
79
+ function rightRotate(value, amount) {
80
+ return (value >>> amount) | (value << (32 - amount));
81
+ }
82
+ const hash = new Uint32Array([
83
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
84
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
85
+ ]);
86
+ const k = new Uint32Array([
87
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
88
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
89
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
90
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
91
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
92
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
93
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
94
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
95
+ ]);
96
+ const len = bytes.length;
97
+ const paddedLen = ((len + 8 + 64) >>> 6) << 6;
98
+ const padded = new Uint8Array(paddedLen);
99
+ padded.set(bytes);
100
+ padded[len] = 0x80;
101
+ const view = new DataView(padded.buffer);
102
+ const bitLen = len * 8;
103
+ view.setUint32(paddedLen - 4, bitLen & 0xffffffff);
104
+ view.setUint32(paddedLen - 8, Math.floor(bitLen / 0x100000000));
105
+ const w = new Uint32Array(64);
106
+ for (let i = 0; i < paddedLen; i += 64) {
107
+ for (let t = 0; t < 16; t++) {
108
+ w[t] = view.getUint32(i + t * 4);
109
+ }
110
+ for (let t = 16; t < 64; t++) {
111
+ const s0 = rightRotate(w[t - 15], 7) ^ rightRotate(w[t - 15], 18) ^ (w[t - 15] >>> 3);
112
+ const s1 = rightRotate(w[t - 2], 17) ^ rightRotate(w[t - 2], 19) ^ (w[t - 2] >>> 10);
113
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) | 0;
114
+ }
115
+ let a = hash[0];
116
+ let b = hash[1];
117
+ let c = hash[2];
118
+ let d = hash[3];
119
+ let e = hash[4];
120
+ let f = hash[5];
121
+ let g = hash[6];
122
+ let h = hash[7];
123
+ for (let t = 0; t < 64; t++) {
124
+ const s1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
125
+ const ch = (e & f) ^ (~e & g);
126
+ const temp1 = (h + s1 + ch + k[t] + w[t]) | 0;
127
+ const s0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
128
+ const maj = (a & b) ^ (a & c) ^ (b & c);
129
+ const temp2 = (s0 + maj) | 0;
130
+ h = g;
131
+ g = f;
132
+ f = e;
133
+ e = (d + temp1) | 0;
134
+ d = c;
135
+ c = b;
136
+ b = a;
137
+ a = (temp1 + temp2) | 0;
138
+ }
139
+ hash[0] = (hash[0] + a) | 0;
140
+ hash[1] = (hash[1] + b) | 0;
141
+ hash[2] = (hash[2] + c) | 0;
142
+ hash[3] = (hash[3] + d) | 0;
143
+ hash[4] = (hash[4] + e) | 0;
144
+ hash[5] = (hash[5] + f) | 0;
145
+ hash[6] = (hash[6] + g) | 0;
146
+ hash[7] = (hash[7] + h) | 0;
147
+ }
148
+ const result = new Uint8Array(32);
149
+ const resultView = new DataView(result.buffer);
150
+ for (let i = 0; i < 8; i++) {
151
+ resultView.setUint32(i * 4, hash[i]);
152
+ }
153
+ return result;
154
+ }
155
+ function hmacSha256Pure(secret, message) {
156
+ const encoder = new TextEncoder();
157
+ let key = encoder.encode(secret);
158
+ const msgBytes = encoder.encode(message);
159
+ const blockValues = 64;
160
+ if (key.length > blockValues) {
161
+ key = new Uint8Array(sha256Pure(key));
162
+ }
163
+ const paddedKey = new Uint8Array(blockValues);
164
+ paddedKey.set(key);
165
+ const ipad = new Uint8Array(blockValues);
166
+ const opad = new Uint8Array(blockValues);
167
+ for (let i = 0; i < blockValues; i++) {
168
+ ipad[i] = paddedKey[i] ^ 0x36;
169
+ opad[i] = paddedKey[i] ^ 0x5c;
170
+ }
171
+ const innerInput = new Uint8Array(blockValues + msgBytes.length);
172
+ innerInput.set(ipad, 0);
173
+ innerInput.set(msgBytes, blockValues);
174
+ const innerHash = sha256Pure(innerInput);
175
+ const outerInput = new Uint8Array(blockValues + 32);
176
+ outerInput.set(opad, 0);
177
+ outerInput.set(innerHash, blockValues);
178
+ return sha256Pure(outerInput);
179
+ }
180
+ /**
181
+ * Verify browser-native HMAC-SHA256 signature.
182
+ */
183
+ export async function verifyHmacSha256(secret, canonicalString, signatureHex) {
184
+ try {
185
+ const computedHex = await signHmacSha256(secret, canonicalString);
186
+ return computedHex === signatureHex;
187
+ }
188
+ catch (e) {
189
+ return false;
190
+ }
191
+ }