@flrande/bak-extension 0.6.11 → 0.6.13
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/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +588 -90
- package/dist/content.global.js +530 -12
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +233 -101
- package/dist/popup.html +260 -37
- package/package.json +2 -2
- package/public/popup.html +260 -37
- package/src/background.ts +344 -296
- package/src/content.ts +390 -22
- package/src/dynamic-data-tools.ts +790 -0
- package/src/popup.ts +291 -108
|
@@ -1,5 +1,499 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
+
// src/dynamic-data-tools.ts
|
|
4
|
+
var DATA_PATTERN = /\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|freshness|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\b/i;
|
|
5
|
+
var CONTRACT_PATTERN = /\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\b/i;
|
|
6
|
+
var EVENT_PATTERN = /\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\b/i;
|
|
7
|
+
var ROW_CANDIDATE_KEYS = ["data", "rows", "results", "items", "records", "entries"];
|
|
8
|
+
function normalizeColumnName(value) {
|
|
9
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
10
|
+
}
|
|
11
|
+
function normalizedComparableValue(value) {
|
|
12
|
+
if (value === null || value === void 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "object") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const text = String(value).trim().toLowerCase();
|
|
19
|
+
return text.length > 0 ? text : null;
|
|
20
|
+
}
|
|
21
|
+
function compareNumbers(left, right) {
|
|
22
|
+
return left - right;
|
|
23
|
+
}
|
|
24
|
+
function latestTimestamp(timestamps) {
|
|
25
|
+
const values = timestamps.map((timestamp) => Date.parse(timestamp.value)).filter((value) => Number.isFinite(value));
|
|
26
|
+
return values.length > 0 ? Math.max(...values) : null;
|
|
27
|
+
}
|
|
28
|
+
function sampleValue(value, depth = 0) {
|
|
29
|
+
if (depth >= 2 || value === null || value === void 0 || typeof value !== "object") {
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
return value.length > 160 ? value.slice(0, 160) : value;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "function") {
|
|
34
|
+
return "[Function]";
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
return value.slice(0, 3).map((item) => sampleValue(item, depth + 1));
|
|
40
|
+
}
|
|
41
|
+
const sampled = {};
|
|
42
|
+
for (const key of Object.keys(value).slice(0, 8)) {
|
|
43
|
+
try {
|
|
44
|
+
sampled[key] = sampleValue(value[key], depth + 1);
|
|
45
|
+
} catch {
|
|
46
|
+
sampled[key] = "[Unreadable]";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return sampled;
|
|
50
|
+
}
|
|
51
|
+
function estimateSampleSize(value) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.length;
|
|
54
|
+
}
|
|
55
|
+
if (value && typeof value === "object") {
|
|
56
|
+
return Object.keys(value).length;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function classifyTimestamp(path, value, now = Date.now()) {
|
|
61
|
+
const normalized = path.toLowerCase();
|
|
62
|
+
if (DATA_PATTERN.test(normalized)) {
|
|
63
|
+
return "data";
|
|
64
|
+
}
|
|
65
|
+
if (CONTRACT_PATTERN.test(normalized)) {
|
|
66
|
+
return "contract";
|
|
67
|
+
}
|
|
68
|
+
if (EVENT_PATTERN.test(normalized)) {
|
|
69
|
+
return "event";
|
|
70
|
+
}
|
|
71
|
+
const parsed = Date.parse(value.trim());
|
|
72
|
+
return Number.isFinite(parsed) && parsed > now + 36 * 60 * 60 * 1e3 ? "contract" : "unknown";
|
|
73
|
+
}
|
|
74
|
+
function collectTimestampProbes(value, path, options = {}) {
|
|
75
|
+
const collected = [];
|
|
76
|
+
const now = typeof options.now === "number" ? options.now : Date.now();
|
|
77
|
+
const limit = typeof options.limit === "number" ? Math.max(1, Math.floor(options.limit)) : 16;
|
|
78
|
+
const visit = (candidate, candidatePath, depth) => {
|
|
79
|
+
if (collected.length >= limit) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
83
|
+
const parsed = Date.parse(candidate.trim());
|
|
84
|
+
if (Number.isFinite(parsed)) {
|
|
85
|
+
collected.push({
|
|
86
|
+
path: candidatePath,
|
|
87
|
+
value: candidate,
|
|
88
|
+
category: classifyTimestamp(candidatePath, candidate, now)
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (depth >= 3 || candidate === null || candidate === void 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(candidate)) {
|
|
97
|
+
candidate.slice(0, 3).forEach((entry, index) => visit(entry, `${candidatePath}[${index}]`, depth + 1));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof candidate === "object") {
|
|
101
|
+
Object.keys(candidate).slice(0, 8).forEach((key) => {
|
|
102
|
+
try {
|
|
103
|
+
visit(candidate[key], candidatePath ? `${candidatePath}.${key}` : key, depth + 1);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
visit(value, path, 0);
|
|
110
|
+
return collected;
|
|
111
|
+
}
|
|
112
|
+
function inferSchemaHint(value) {
|
|
113
|
+
const rowsCandidate = extractStructuredRows(value);
|
|
114
|
+
if (rowsCandidate) {
|
|
115
|
+
if (rowsCandidate.rowType === "object") {
|
|
116
|
+
const firstRecord = rowsCandidate.rows.find(
|
|
117
|
+
(row) => typeof row === "object" && row !== null && !Array.isArray(row)
|
|
118
|
+
);
|
|
119
|
+
return {
|
|
120
|
+
kind: "rows-object",
|
|
121
|
+
columns: firstRecord ? Object.keys(firstRecord).slice(0, 12) : []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (rowsCandidate.rowType === "array") {
|
|
125
|
+
const firstRow = rowsCandidate.rows.find((row) => Array.isArray(row));
|
|
126
|
+
return {
|
|
127
|
+
kind: "rows-array",
|
|
128
|
+
columns: firstRow ? firstRow.map((_, index) => `Column ${index + 1}`) : []
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return { kind: "array" };
|
|
134
|
+
}
|
|
135
|
+
if (value && typeof value === "object") {
|
|
136
|
+
return {
|
|
137
|
+
kind: "object",
|
|
138
|
+
columns: Object.keys(value).slice(0, 12)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (value === null || value === void 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return { kind: "scalar" };
|
|
145
|
+
}
|
|
146
|
+
function extractStructuredRows(value, path = "$") {
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
if (value.length === 0) {
|
|
149
|
+
return { rows: value, path, rowType: "object" };
|
|
150
|
+
}
|
|
151
|
+
const first = value.find((item) => item !== null && item !== void 0);
|
|
152
|
+
if (Array.isArray(first)) {
|
|
153
|
+
return { rows: value, path, rowType: "array" };
|
|
154
|
+
}
|
|
155
|
+
if (first && typeof first === "object") {
|
|
156
|
+
return { rows: value, path, rowType: "object" };
|
|
157
|
+
}
|
|
158
|
+
return { rows: value, path, rowType: "scalar" };
|
|
159
|
+
}
|
|
160
|
+
if (!value || typeof value !== "object") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const record = value;
|
|
164
|
+
for (const key of ROW_CANDIDATE_KEYS) {
|
|
165
|
+
if (Array.isArray(record[key])) {
|
|
166
|
+
return extractStructuredRows(record[key], `${path}.${key}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function toObjectRow(row, fallbackColumns = []) {
|
|
172
|
+
if (row && typeof row === "object" && !Array.isArray(row)) {
|
|
173
|
+
return row;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(row)) {
|
|
176
|
+
const mapped = {};
|
|
177
|
+
row.forEach((value, index) => {
|
|
178
|
+
mapped[fallbackColumns[index] ?? `Column ${index + 1}`] = value;
|
|
179
|
+
});
|
|
180
|
+
return mapped;
|
|
181
|
+
}
|
|
182
|
+
if (row === null || row === void 0) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return { Value: row };
|
|
186
|
+
}
|
|
187
|
+
function sampleRowsFromValue(value, limit = 5) {
|
|
188
|
+
const rowsCandidate = extractStructuredRows(value);
|
|
189
|
+
if (!rowsCandidate) {
|
|
190
|
+
const singleRow = toObjectRow(value);
|
|
191
|
+
return singleRow ? [singleRow] : [];
|
|
192
|
+
}
|
|
193
|
+
const fallbackColumns = rowsCandidate.rowType === "array" ? Array.from({ length: Array.isArray(rowsCandidate.rows[0]) ? rowsCandidate.rows[0].length : 0 }, (_, index) => `Column ${index + 1}`) : [];
|
|
194
|
+
return rowsCandidate.rows.slice(0, limit).map((row) => toObjectRow(row, fallbackColumns)).filter((row) => row !== null);
|
|
195
|
+
}
|
|
196
|
+
function collectSampleValues(rows) {
|
|
197
|
+
const values = /* @__PURE__ */ new Set();
|
|
198
|
+
for (const row of rows) {
|
|
199
|
+
for (const value of Object.values(row)) {
|
|
200
|
+
const comparable = normalizedComparableValue(value);
|
|
201
|
+
if (comparable) {
|
|
202
|
+
values.add(comparable);
|
|
203
|
+
}
|
|
204
|
+
if (values.size >= 24) {
|
|
205
|
+
return values;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return values;
|
|
210
|
+
}
|
|
211
|
+
function buildSourceAnalysis(source, sample) {
|
|
212
|
+
const sampleRows = sampleRowsFromValue(sample);
|
|
213
|
+
return {
|
|
214
|
+
source,
|
|
215
|
+
sampleRows,
|
|
216
|
+
sampleValues: collectSampleValues(sampleRows),
|
|
217
|
+
schemaColumns: source.schemaHint?.columns?.map(normalizeColumnName).filter(Boolean) ?? []
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function parseNetworkBody(entry) {
|
|
221
|
+
const preview = typeof entry.responseBodyPreview === "string" ? entry.responseBodyPreview.trim() : "";
|
|
222
|
+
if (!preview || entry.responseBodyTruncated === true || entry.binary === true) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const contentType = typeof entry.contentType === "string" ? entry.contentType.toLowerCase() : "";
|
|
226
|
+
if (!contentType.includes("json") && !preview.startsWith("{") && !preview.startsWith("[")) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(preview);
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function buildWindowSources(candidates) {
|
|
236
|
+
return candidates.map((candidate) => {
|
|
237
|
+
const source = {
|
|
238
|
+
sourceId: `windowGlobal:${candidate.name}`,
|
|
239
|
+
type: "windowGlobal",
|
|
240
|
+
label: candidate.name,
|
|
241
|
+
path: candidate.name,
|
|
242
|
+
sampleSize: candidate.sampleSize,
|
|
243
|
+
schemaHint: candidate.schemaHint,
|
|
244
|
+
lastObservedAt: candidate.lastObservedAt
|
|
245
|
+
};
|
|
246
|
+
return buildSourceAnalysis(source, candidate.sample);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
function buildInlineJsonAnalyses(sources) {
|
|
250
|
+
return sources.map((sourceItem, index) => {
|
|
251
|
+
const source = {
|
|
252
|
+
sourceId: `inlineJson:${index + 1}:${sourceItem.path}`,
|
|
253
|
+
type: "inlineJson",
|
|
254
|
+
label: sourceItem.label,
|
|
255
|
+
path: sourceItem.path,
|
|
256
|
+
sampleSize: sourceItem.sampleSize,
|
|
257
|
+
schemaHint: sourceItem.schemaHint,
|
|
258
|
+
lastObservedAt: sourceItem.lastObservedAt
|
|
259
|
+
};
|
|
260
|
+
return buildSourceAnalysis(source, sourceItem.sample);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function buildNetworkAnalyses(entries) {
|
|
264
|
+
const analyses = [];
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
const parsed = parseNetworkBody(entry);
|
|
267
|
+
if (parsed === null) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const rowsCandidate = extractStructuredRows(parsed);
|
|
271
|
+
const schemaHint = inferSchemaHint(parsed);
|
|
272
|
+
const url = new URL(entry.url, "http://127.0.0.1");
|
|
273
|
+
const source = {
|
|
274
|
+
sourceId: `networkResponse:${entry.id}`,
|
|
275
|
+
type: "networkResponse",
|
|
276
|
+
label: `${entry.method} ${url.pathname}`,
|
|
277
|
+
path: rowsCandidate?.path ?? url.pathname,
|
|
278
|
+
sampleSize: estimateSampleSize(rowsCandidate?.rows ?? parsed),
|
|
279
|
+
schemaHint,
|
|
280
|
+
lastObservedAt: entry.ts
|
|
281
|
+
};
|
|
282
|
+
analyses.push(buildSourceAnalysis(source, rowsCandidate?.rows ?? parsed));
|
|
283
|
+
}
|
|
284
|
+
return analyses;
|
|
285
|
+
}
|
|
286
|
+
function scoreSourceMapping(table, source, now) {
|
|
287
|
+
const tableColumns = table.schema.columns.map((column) => column.label);
|
|
288
|
+
const normalizedTableColumns = new Map(tableColumns.map((label) => [normalizeColumnName(label), label]));
|
|
289
|
+
const matchedColumns = [...new Set(source.schemaColumns.filter((column) => normalizedTableColumns.has(column)).map((column) => normalizedTableColumns.get(column)))];
|
|
290
|
+
const basis = [];
|
|
291
|
+
if (matchedColumns.length > 0) {
|
|
292
|
+
basis.push({
|
|
293
|
+
type: "columnOverlap",
|
|
294
|
+
detail: `Column overlap on ${matchedColumns.join(", ")}`
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const overlappingValues = [...table.sampleRows.flatMap((row) => Object.values(row))].map((value) => normalizedComparableValue(value)).filter((value) => value !== null).filter((value) => source.sampleValues.has(value));
|
|
298
|
+
const distinctOverlappingValues = [...new Set(overlappingValues)].slice(0, 5);
|
|
299
|
+
if (distinctOverlappingValues.length > 0) {
|
|
300
|
+
basis.push({
|
|
301
|
+
type: "sampleValueOverlap",
|
|
302
|
+
detail: `Shared sample values: ${distinctOverlappingValues.join(", ")}`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const explicitReferenceHit = table.table.name.toLowerCase().includes(source.source.label.toLowerCase()) || (table.table.selector ?? "").toLowerCase().includes(source.source.label.toLowerCase()) || source.source.label.toLowerCase().includes(table.table.name.toLowerCase());
|
|
306
|
+
if (explicitReferenceHit) {
|
|
307
|
+
basis.push({
|
|
308
|
+
type: "explicitReference",
|
|
309
|
+
detail: `Table label and source label both mention ${source.source.label}`
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (source.source.type === "networkResponse" && typeof source.source.lastObservedAt === "number" && Math.max(0, now - source.source.lastObservedAt) <= 9e4) {
|
|
313
|
+
basis.push({
|
|
314
|
+
type: "timeProximity",
|
|
315
|
+
detail: "Recent network response observed within the last 90 seconds"
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (basis.length === 0) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const confidence = matchedColumns.length >= Math.max(2, Math.min(tableColumns.length, 3)) || matchedColumns.length > 0 && distinctOverlappingValues.length > 0 ? "high" : matchedColumns.length > 0 || distinctOverlappingValues.length > 0 ? "medium" : "low";
|
|
322
|
+
return {
|
|
323
|
+
tableId: table.table.id,
|
|
324
|
+
sourceId: source.source.sourceId,
|
|
325
|
+
confidence,
|
|
326
|
+
basis,
|
|
327
|
+
matchedColumns
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function buildRecommendedNextActions(tables, mappings, sourceAnalyses) {
|
|
331
|
+
const recommendations = [];
|
|
332
|
+
const pushRecommendation = (item) => {
|
|
333
|
+
if (recommendations.some((existing) => existing.command === item.command)) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
recommendations.push(item);
|
|
337
|
+
};
|
|
338
|
+
for (const table of tables) {
|
|
339
|
+
if (table.table.intelligence?.preferredExtractionMode === "scroll") {
|
|
340
|
+
pushRecommendation({
|
|
341
|
+
title: `Read all rows from ${table.table.id}`,
|
|
342
|
+
command: `bak table rows --table ${table.table.id} --all --max-rows 10000`,
|
|
343
|
+
note: "The table looks virtualized or lazy-loaded, so a scroll pass is preferred."
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const mapping of mappings.filter((item) => item.confidence !== "low")) {
|
|
348
|
+
const source = sourceAnalyses.find((analysis) => analysis.source.sourceId === mapping.sourceId);
|
|
349
|
+
if (!source) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (source.source.type === "windowGlobal") {
|
|
353
|
+
pushRecommendation({
|
|
354
|
+
title: `Read ${source.source.label} directly from page data`,
|
|
355
|
+
command: `bak page extract --path "${source.source.path}" --resolver auto`,
|
|
356
|
+
note: `Mapped to ${mapping.tableId} with ${mapping.confidence} confidence.`
|
|
357
|
+
});
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (source.source.type === "networkResponse") {
|
|
361
|
+
const requestId = source.source.sourceId.replace(/^networkResponse:/, "");
|
|
362
|
+
pushRecommendation({
|
|
363
|
+
title: `Replay ${requestId} with table schema`,
|
|
364
|
+
command: `bak network replay --request-id ${requestId} --mode json --with-schema auto`,
|
|
365
|
+
note: `Recent response mapped to ${mapping.tableId} with ${mapping.confidence} confidence.`
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
pushRecommendation({
|
|
370
|
+
title: `Inspect ${source.source.label} inline JSON`,
|
|
371
|
+
command: "bak page freshness",
|
|
372
|
+
note: `Inline JSON source mapped to ${mapping.tableId}; use freshness or capture commands to inspect it further.`
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (recommendations.length === 0) {
|
|
376
|
+
pushRecommendation({
|
|
377
|
+
title: "Check data freshness",
|
|
378
|
+
command: "bak page freshness",
|
|
379
|
+
note: "No strong data-source mapping was found yet."
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return recommendations.slice(0, 6);
|
|
383
|
+
}
|
|
384
|
+
function buildSourceMappingReport(input) {
|
|
385
|
+
const now = typeof input.now === "number" ? input.now : Date.now();
|
|
386
|
+
const windowAnalyses = buildWindowSources(input.windowSources);
|
|
387
|
+
const inlineAnalyses = buildInlineJsonAnalyses(input.inlineJsonSources);
|
|
388
|
+
const networkAnalyses = buildNetworkAnalyses(input.recentNetwork);
|
|
389
|
+
const sourceAnalyses = [...windowAnalyses, ...inlineAnalyses, ...networkAnalyses];
|
|
390
|
+
const sourceMappings = input.tables.flatMap((table) => sourceAnalyses.map((source) => scoreSourceMapping(table, source, now))).filter((mapping) => mapping !== null).sort((left, right) => {
|
|
391
|
+
const confidenceRank = { high: 0, medium: 1, low: 2 };
|
|
392
|
+
return confidenceRank[left.confidence] - confidenceRank[right.confidence] || left.tableId.localeCompare(right.tableId) || left.sourceId.localeCompare(right.sourceId);
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
dataSources: sourceAnalyses.map((analysis) => analysis.source),
|
|
396
|
+
sourceMappings,
|
|
397
|
+
recommendedNextActions: buildRecommendedNextActions(input.tables, sourceMappings, sourceAnalyses),
|
|
398
|
+
sourceAnalyses
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function mapObjectRowToSchema(row, schema) {
|
|
402
|
+
const normalizedKeys = new Map(Object.keys(row).map((key) => [normalizeColumnName(key), key]));
|
|
403
|
+
const mapped = {};
|
|
404
|
+
for (const column of schema.columns) {
|
|
405
|
+
const normalized = normalizeColumnName(column.label);
|
|
406
|
+
const sourceKey = normalizedKeys.get(normalized);
|
|
407
|
+
if (sourceKey) {
|
|
408
|
+
mapped[column.label] = row[sourceKey];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (Object.keys(mapped).length > 0) {
|
|
412
|
+
return mapped;
|
|
413
|
+
}
|
|
414
|
+
return { ...row };
|
|
415
|
+
}
|
|
416
|
+
function selectReplaySchemaMatch(responseJson, tables, options = {}) {
|
|
417
|
+
const candidate = extractStructuredRows(responseJson);
|
|
418
|
+
if (!candidate || candidate.rows.length === 0 || tables.length === 0) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const preferredTableId = options.preferredSourceId && options.mappings ? options.mappings.find((mapping) => mapping.sourceId === options.preferredSourceId && mapping.confidence !== "low")?.tableId : void 0;
|
|
422
|
+
const orderedTables = preferredTableId ? tables.slice().sort((left, right) => {
|
|
423
|
+
if (left.table.id === preferredTableId) {
|
|
424
|
+
return -1;
|
|
425
|
+
}
|
|
426
|
+
if (right.table.id === preferredTableId) {
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
return left.table.id.localeCompare(right.table.id);
|
|
430
|
+
}) : tables;
|
|
431
|
+
const firstRow = candidate.rows[0];
|
|
432
|
+
if (Array.isArray(firstRow)) {
|
|
433
|
+
const matchingTable = orderedTables.find((table) => table.schema.columns.length === firstRow.length) ?? orderedTables.find((table) => table.schema.columns.length > 0) ?? null;
|
|
434
|
+
if (!matchingTable) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
table: matchingTable.table,
|
|
439
|
+
schema: matchingTable.schema,
|
|
440
|
+
mappedRows: candidate.rows.filter((row) => Array.isArray(row)).map((row) => {
|
|
441
|
+
const mapped = {};
|
|
442
|
+
matchingTable.schema.columns.forEach((column, index) => {
|
|
443
|
+
mapped[column.label] = row[index];
|
|
444
|
+
});
|
|
445
|
+
return mapped;
|
|
446
|
+
}),
|
|
447
|
+
mappingSource: candidate.path
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (firstRow && typeof firstRow === "object") {
|
|
451
|
+
const rowObject = firstRow;
|
|
452
|
+
const rowKeys = new Set(Object.keys(rowObject).map(normalizeColumnName));
|
|
453
|
+
const matchingEntry = orderedTables.map((table) => ({
|
|
454
|
+
table,
|
|
455
|
+
score: table.schema.columns.filter((column) => rowKeys.has(normalizeColumnName(column.label))).length
|
|
456
|
+
})).sort((left, right) => compareNumbers(right.score, left.score))[0] ?? null;
|
|
457
|
+
if (!matchingEntry || matchingEntry.score <= 0) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const matchingTable = matchingEntry.table;
|
|
461
|
+
return {
|
|
462
|
+
table: matchingTable.table,
|
|
463
|
+
schema: matchingTable.schema,
|
|
464
|
+
mappedRows: candidate.rows.filter((row) => typeof row === "object" && row !== null && !Array.isArray(row)).map((row) => mapObjectRowToSchema(row, matchingTable.schema)),
|
|
465
|
+
mappingSource: candidate.path
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
function buildInspectPageDataResult(input) {
|
|
471
|
+
const report = buildSourceMappingReport({
|
|
472
|
+
tables: input.tableAnalyses,
|
|
473
|
+
windowSources: input.pageDataCandidates,
|
|
474
|
+
inlineJsonSources: input.inlineJsonSources,
|
|
475
|
+
recentNetwork: input.recentNetwork,
|
|
476
|
+
now: input.now
|
|
477
|
+
});
|
|
478
|
+
return {
|
|
479
|
+
dataSources: report.dataSources,
|
|
480
|
+
sourceMappings: report.sourceMappings,
|
|
481
|
+
recommendedNextActions: report.recommendedNextActions
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function buildPageDataProbe(name, resolver, sample) {
|
|
485
|
+
const timestamps = collectTimestampProbes(sample, name);
|
|
486
|
+
return {
|
|
487
|
+
name,
|
|
488
|
+
resolver,
|
|
489
|
+
sample: sampleValue(sample),
|
|
490
|
+
sampleSize: estimateSampleSize(sample),
|
|
491
|
+
schemaHint: inferSchemaHint(sample),
|
|
492
|
+
lastObservedAt: latestTimestamp(timestamps),
|
|
493
|
+
timestamps
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
3
497
|
// src/privacy.ts
|
|
4
498
|
var HIGH_ENTROPY_TOKEN_PATTERN = /^(?=.*\d)(?=.*[a-zA-Z])[A-Za-z0-9~!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`]{16,}$/;
|
|
5
499
|
var TRANSPORT_SECRET_KEY_SOURCE = "(?:api[_-]?key|authorization|auth|cookie|csrf(?:token)?|nonce|password|passwd|secret|session(?:id)?|token|xsrf(?:token)?)";
|
|
@@ -62,7 +556,7 @@
|
|
|
62
556
|
// package.json
|
|
63
557
|
var package_default = {
|
|
64
558
|
name: "@flrande/bak-extension",
|
|
65
|
-
version: "0.6.
|
|
559
|
+
version: "0.6.13",
|
|
66
560
|
type: "module",
|
|
67
561
|
scripts: {
|
|
68
562
|
build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
|
|
@@ -1414,6 +1908,7 @@
|
|
|
1414
1908
|
var preserveHumanFocusDepth = 0;
|
|
1415
1909
|
var lastBindingUpdateAt = null;
|
|
1416
1910
|
var lastBindingUpdateReason = null;
|
|
1911
|
+
var bindingUpdateMetadata = /* @__PURE__ */ new Map();
|
|
1417
1912
|
async function getConfig() {
|
|
1418
1913
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
1419
1914
|
return {
|
|
@@ -1546,19 +2041,27 @@
|
|
|
1546
2041
|
async function listSessionBindingStates() {
|
|
1547
2042
|
return Object.values(await loadSessionBindingStateMap());
|
|
1548
2043
|
}
|
|
1549
|
-
function summarizeSessionBindings(states) {
|
|
1550
|
-
const items =
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2044
|
+
async function summarizeSessionBindings(states) {
|
|
2045
|
+
const items = await Promise.all(
|
|
2046
|
+
states.map(async (state) => {
|
|
2047
|
+
const detached = state.windowId === null || state.tabIds.length === 0;
|
|
2048
|
+
const activeTab = typeof state.activeTabId === "number" ? await sessionBindingBrowser.getTab(state.activeTabId) : null;
|
|
2049
|
+
const bindingUpdate = bindingUpdateMetadata.get(state.id);
|
|
2050
|
+
return {
|
|
2051
|
+
id: state.id,
|
|
2052
|
+
label: state.label,
|
|
2053
|
+
tabCount: state.tabIds.length,
|
|
2054
|
+
activeTabId: state.activeTabId,
|
|
2055
|
+
activeTabTitle: activeTab?.title ?? null,
|
|
2056
|
+
activeTabUrl: activeTab?.url ?? null,
|
|
2057
|
+
windowId: state.windowId,
|
|
2058
|
+
groupId: state.groupId,
|
|
2059
|
+
detached,
|
|
2060
|
+
lastBindingUpdateAt: bindingUpdate?.at ?? null,
|
|
2061
|
+
lastBindingUpdateReason: bindingUpdate?.reason ?? null
|
|
2062
|
+
};
|
|
2063
|
+
})
|
|
2064
|
+
);
|
|
1562
2065
|
return {
|
|
1563
2066
|
count: items.length,
|
|
1564
2067
|
attachedCount: items.filter((item) => !item.detached).length,
|
|
@@ -1569,7 +2072,7 @@
|
|
|
1569
2072
|
}
|
|
1570
2073
|
async function buildPopupState() {
|
|
1571
2074
|
const config = await getConfig();
|
|
1572
|
-
const sessionBindings = summarizeSessionBindings(await listSessionBindingStates());
|
|
2075
|
+
const sessionBindings = await summarizeSessionBindings(await listSessionBindingStates());
|
|
1573
2076
|
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
1574
2077
|
let connectionState;
|
|
1575
2078
|
if (!config.token) {
|
|
@@ -1630,6 +2133,10 @@
|
|
|
1630
2133
|
function emitSessionBindingUpdated(bindingId, reason, state, extras = {}) {
|
|
1631
2134
|
lastBindingUpdateAt = Date.now();
|
|
1632
2135
|
lastBindingUpdateReason = reason;
|
|
2136
|
+
bindingUpdateMetadata.set(bindingId, {
|
|
2137
|
+
at: lastBindingUpdateAt,
|
|
2138
|
+
reason
|
|
2139
|
+
});
|
|
1633
2140
|
sendEvent("sessionBinding.updated", {
|
|
1634
2141
|
bindingId,
|
|
1635
2142
|
reason,
|
|
@@ -2482,15 +2989,6 @@
|
|
|
2482
2989
|
const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
|
|
2483
2990
|
const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
|
|
2484
2991
|
const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
|
|
2485
|
-
const isTimestampString = (value) => typeof value === 'string' && value.trim().length > 0 && !Number.isNaN(Date.parse(value.trim()));
|
|
2486
|
-
const classify = (path, value) => {
|
|
2487
|
-
const normalized = String(path || '').toLowerCase();
|
|
2488
|
-
if (dataPattern.test(normalized)) return 'data';
|
|
2489
|
-
if (contractPattern.test(normalized)) return 'contract';
|
|
2490
|
-
if (eventPattern.test(normalized)) return 'event';
|
|
2491
|
-
const parsed = Date.parse(String(value || '').trim());
|
|
2492
|
-
return Number.isFinite(parsed) && parsed > Date.now() + 36 * 60 * 60 * 1000 ? 'contract' : 'unknown';
|
|
2493
|
-
};
|
|
2494
2992
|
const sampleValue = (value, depth = 0) => {
|
|
2495
2993
|
if (depth >= 2 || value == null || typeof value !== 'object') {
|
|
2496
2994
|
if (typeof value === 'string') {
|
|
@@ -2514,29 +3012,6 @@
|
|
|
2514
3012
|
}
|
|
2515
3013
|
return sampled;
|
|
2516
3014
|
};
|
|
2517
|
-
const collectTimestamps = (value, path, depth, collected) => {
|
|
2518
|
-
if (collected.length >= 16) return;
|
|
2519
|
-
if (isTimestampString(value)) {
|
|
2520
|
-
collected.push({ path, value: String(value), category: classify(path, value) });
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
if (depth >= 3) return;
|
|
2524
|
-
if (Array.isArray(value)) {
|
|
2525
|
-
value.slice(0, 3).forEach((item, index) => collectTimestamps(item, path + '[' + index + ']', depth + 1, collected));
|
|
2526
|
-
return;
|
|
2527
|
-
}
|
|
2528
|
-
if (value && typeof value === 'object') {
|
|
2529
|
-
Object.keys(value)
|
|
2530
|
-
.slice(0, 8)
|
|
2531
|
-
.forEach((key) => {
|
|
2532
|
-
try {
|
|
2533
|
-
collectTimestamps(value[key], path ? path + '.' + key : key, depth + 1, collected);
|
|
2534
|
-
} catch {
|
|
2535
|
-
// Ignore unreadable nested properties.
|
|
2536
|
-
}
|
|
2537
|
-
});
|
|
2538
|
-
}
|
|
2539
|
-
};
|
|
2540
3015
|
const readCandidate = (name) => {
|
|
2541
3016
|
if (name in globalThis) {
|
|
2542
3017
|
return { resolver: 'globalThis', value: globalThis[name] };
|
|
@@ -2547,13 +3022,10 @@
|
|
|
2547
3022
|
for (const name of candidates) {
|
|
2548
3023
|
try {
|
|
2549
3024
|
const resolved = readCandidate(name);
|
|
2550
|
-
const timestamps = [];
|
|
2551
|
-
collectTimestamps(resolved.value, name, 0, timestamps);
|
|
2552
3025
|
results.push({
|
|
2553
3026
|
name,
|
|
2554
3027
|
resolver: resolved.resolver,
|
|
2555
|
-
sample: sampleValue(resolved.value)
|
|
2556
|
-
timestamps
|
|
3028
|
+
sample: sampleValue(resolved.value)
|
|
2557
3029
|
});
|
|
2558
3030
|
} catch {
|
|
2559
3031
|
// Ignore inaccessible candidates.
|
|
@@ -2568,7 +3040,12 @@
|
|
|
2568
3040
|
maxBytes: 64 * 1024
|
|
2569
3041
|
});
|
|
2570
3042
|
const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
|
|
2571
|
-
|
|
3043
|
+
if (!Array.isArray(frameResult?.value)) {
|
|
3044
|
+
return [];
|
|
3045
|
+
}
|
|
3046
|
+
return frameResult.value.filter(
|
|
3047
|
+
(candidate) => typeof candidate === "object" && candidate !== null && typeof candidate.name === "string"
|
|
3048
|
+
).map((candidate) => buildPageDataProbe(candidate.name, candidate.resolver, candidate.sample));
|
|
2572
3049
|
} catch {
|
|
2573
3050
|
return [];
|
|
2574
3051
|
}
|
|
@@ -2692,52 +3169,59 @@
|
|
|
2692
3169
|
}
|
|
2693
3170
|
return null;
|
|
2694
3171
|
}
|
|
2695
|
-
async function
|
|
2696
|
-
const candidate = extractReplayRowsCandidate(response.json);
|
|
2697
|
-
if (!candidate || candidate.rows.length === 0) {
|
|
2698
|
-
return response;
|
|
2699
|
-
}
|
|
2700
|
-
const firstRow = candidate.rows[0];
|
|
3172
|
+
async function collectTableAnalyses(tabId) {
|
|
2701
3173
|
const tablesResult = await forwardContentRpc(tabId, "table.list", {});
|
|
2702
3174
|
const tables = Array.isArray(tablesResult.tables) ? tablesResult.tables : [];
|
|
2703
|
-
|
|
2704
|
-
return response;
|
|
2705
|
-
}
|
|
2706
|
-
const schemas = [];
|
|
3175
|
+
const analyses = [];
|
|
2707
3176
|
for (const table of tables) {
|
|
2708
3177
|
const schemaResult = await forwardContentRpc(tabId, "table.schema", { table: table.id });
|
|
3178
|
+
const rowsResult = await forwardContentRpc(tabId, "table.rows", {
|
|
3179
|
+
table: table.id,
|
|
3180
|
+
limit: 8
|
|
3181
|
+
});
|
|
2709
3182
|
if (schemaResult.schema && Array.isArray(schemaResult.schema.columns)) {
|
|
2710
|
-
|
|
3183
|
+
analyses.push({
|
|
3184
|
+
table: schemaResult.table ?? rowsResult.table ?? table,
|
|
3185
|
+
schema: schemaResult.schema,
|
|
3186
|
+
sampleRows: Array.isArray(rowsResult.rows) ? rowsResult.rows.slice(0, 8) : []
|
|
3187
|
+
});
|
|
2711
3188
|
}
|
|
2712
3189
|
}
|
|
2713
|
-
|
|
3190
|
+
return analyses;
|
|
3191
|
+
}
|
|
3192
|
+
async function enrichReplayWithSchema(tabId, requestId, response) {
|
|
3193
|
+
const candidate = extractReplayRowsCandidate(response.json);
|
|
3194
|
+
if (!candidate || candidate.rows.length === 0) {
|
|
2714
3195
|
return response;
|
|
2715
3196
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
return response;
|
|
2720
|
-
}
|
|
2721
|
-
const mappedRows = candidate.rows.filter((row) => Array.isArray(row)).map((row) => {
|
|
2722
|
-
const mapped = {};
|
|
2723
|
-
matchingSchema.schema.columns.forEach((column, index) => {
|
|
2724
|
-
mapped[column.label] = row[index];
|
|
2725
|
-
});
|
|
2726
|
-
return mapped;
|
|
2727
|
-
});
|
|
2728
|
-
return {
|
|
2729
|
-
...response,
|
|
2730
|
-
table: matchingSchema.table,
|
|
2731
|
-
schema: matchingSchema.schema,
|
|
2732
|
-
mappedRows,
|
|
2733
|
-
mappingSource: candidate.source
|
|
2734
|
-
};
|
|
3197
|
+
const tables = await collectTableAnalyses(tabId);
|
|
3198
|
+
if (tables.length === 0) {
|
|
3199
|
+
return response;
|
|
2735
3200
|
}
|
|
2736
|
-
|
|
3201
|
+
const inspection = await collectPageInspection(tabId, {});
|
|
3202
|
+
const pageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
|
|
3203
|
+
const recentNetwork = listNetworkEntries(tabId, { limit: 25 });
|
|
3204
|
+
const pageDataReport = buildInspectPageDataResult({
|
|
3205
|
+
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
3206
|
+
tables: inspection.tables ?? [],
|
|
3207
|
+
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
3208
|
+
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
3209
|
+
pageDataCandidates,
|
|
3210
|
+
recentNetwork,
|
|
3211
|
+
tableAnalyses: tables,
|
|
3212
|
+
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
|
|
3213
|
+
});
|
|
3214
|
+
const matched = selectReplaySchemaMatch(response.json, tables, {
|
|
3215
|
+
preferredSourceId: `networkResponse:${requestId}`,
|
|
3216
|
+
mappings: pageDataReport.sourceMappings
|
|
3217
|
+
});
|
|
3218
|
+
if (matched) {
|
|
2737
3219
|
return {
|
|
2738
3220
|
...response,
|
|
2739
|
-
|
|
2740
|
-
|
|
3221
|
+
table: matched.table,
|
|
3222
|
+
schema: matched.schema,
|
|
3223
|
+
mappedRows: matched.mappedRows,
|
|
3224
|
+
mappingSource: matched.mappingSource
|
|
2741
3225
|
};
|
|
2742
3226
|
}
|
|
2743
3227
|
return response;
|
|
@@ -3255,7 +3739,7 @@
|
|
|
3255
3739
|
if (!first) {
|
|
3256
3740
|
throw toError("E_EXECUTION", "network replay returned no response payload");
|
|
3257
3741
|
}
|
|
3258
|
-
return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, first) : first;
|
|
3742
|
+
return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, String(params.id ?? ""), first) : first;
|
|
3259
3743
|
});
|
|
3260
3744
|
}
|
|
3261
3745
|
case "page.freshness": {
|
|
@@ -3333,6 +3817,18 @@
|
|
|
3333
3817
|
const inspection = await collectPageInspection(tab.id, params);
|
|
3334
3818
|
const pageDataCandidates = await probePageDataCandidatesForTab(tab.id, inspection);
|
|
3335
3819
|
const network = listNetworkEntries(tab.id, { limit: 10 });
|
|
3820
|
+
const tableAnalyses = await collectTableAnalyses(tab.id);
|
|
3821
|
+
const enriched = buildInspectPageDataResult({
|
|
3822
|
+
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
3823
|
+
tables: inspection.tables ?? [],
|
|
3824
|
+
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
3825
|
+
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
3826
|
+
pageDataCandidates,
|
|
3827
|
+
recentNetwork: network,
|
|
3828
|
+
tableAnalyses,
|
|
3829
|
+
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
|
|
3830
|
+
});
|
|
3831
|
+
const recommendedNextSteps = enriched.recommendedNextActions.map((action) => action.command);
|
|
3336
3832
|
return {
|
|
3337
3833
|
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
3338
3834
|
tables: inspection.tables ?? [],
|
|
@@ -3340,10 +3836,12 @@
|
|
|
3340
3836
|
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
3341
3837
|
pageDataCandidates,
|
|
3342
3838
|
recentNetwork: network,
|
|
3839
|
+
dataSources: enriched.dataSources,
|
|
3840
|
+
sourceMappings: enriched.sourceMappings,
|
|
3841
|
+
recommendedNextActions: enriched.recommendedNextActions,
|
|
3343
3842
|
recommendedNextSteps: [
|
|
3344
|
-
|
|
3345
|
-
"bak
|
|
3346
|
-
"bak page freshness"
|
|
3843
|
+
...recommendedNextSteps,
|
|
3844
|
+
...["bak page freshness"].filter((command) => !recommendedNextSteps.includes(command))
|
|
3347
3845
|
]
|
|
3348
3846
|
};
|
|
3349
3847
|
});
|