@flrande/bak-extension 0.6.0 → 0.6.2

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/src/content.ts CHANGED
@@ -93,6 +93,13 @@ let longTaskDurationMs = 0;
93
93
  let performanceBaselineMs = 0;
94
94
  const pageLoadedAt = Math.round(performance.timeOrigin || Date.now());
95
95
  let lastMutationAt = Date.now();
96
+
97
+ const DISCOVERY_GLOBAL_PATTERN = /(data|table|json|state|store|market|quote|flow|row|timestamp|snapshot|book|signal)/i;
98
+
99
+ interface TimestampCandidateMatch {
100
+ value: string;
101
+ context: string;
102
+ }
96
103
 
97
104
  function isHtmlElement(node: Element | null): node is HTMLElement {
98
105
  if (!node) {
@@ -1692,19 +1699,32 @@ function currentContextSnapshot(): { tabId: null; framePath: string[]; shadowPat
1692
1699
  };
1693
1700
  }
1694
1701
 
1695
- function timestampCandidatesFromText(text: string, patterns?: string[]): string[] {
1696
- const regexes = (patterns ?? [String.raw`\b20\d{2}-\d{2}-\d{2}\b`, String.raw`\b20\d{2}\/\d{2}\/\d{2}\b`]).map(
1697
- (pattern) => new RegExp(pattern, 'gi')
1698
- );
1699
- const collected = new Set<string>();
1702
+ function timestampCandidateMatchesFromText(text: string, patterns?: string[]): TimestampCandidateMatch[] {
1703
+ const regexes = (
1704
+ patterns ?? [
1705
+ String.raw`\b20\d{2}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b`,
1706
+ String.raw`\b20\d{2}-\d{2}-\d{2}\b`,
1707
+ String.raw`\b20\d{2}\/\d{2}\/\d{2}\b`
1708
+ ]
1709
+ ).map((pattern) => new RegExp(pattern, 'gi'));
1710
+ const collected = new Map<string, TimestampCandidateMatch>();
1700
1711
  for (const regex of regexes) {
1701
1712
  for (const match of text.matchAll(regex)) {
1702
- if (match[0]) {
1703
- collected.add(match[0]);
1713
+ const value = match[0];
1714
+ if (!value) {
1715
+ continue;
1716
+ }
1717
+ const index = match.index ?? text.indexOf(value);
1718
+ const start = Math.max(0, index - 28);
1719
+ const end = Math.min(text.length, index + value.length + 28);
1720
+ const context = text.slice(start, end).replace(/\s+/g, ' ').trim();
1721
+ const key = `${value}::${context}`;
1722
+ if (!collected.has(key)) {
1723
+ collected.set(key, { value, context });
1704
1724
  }
1705
1725
  }
1706
1726
  }
1707
- return [...collected];
1727
+ return [...collected.values()];
1708
1728
  }
1709
1729
 
1710
1730
  function listInlineScripts(): Array<{ content: string; suspectedVars: string[] }> {
@@ -1712,9 +1732,9 @@ function listInlineScripts(): Array<{ content: string; suspectedVars: string[] }
1712
1732
  .filter((script) => !script.src)
1713
1733
  .map((script) => {
1714
1734
  const content = script.textContent ?? '';
1715
- const suspectedVars = [...content.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*(?:data|table|json|state|store)[A-Za-z_$\w]*)/gi)].map(
1716
- (match) => match[1] ?? ''
1717
- ).filter(Boolean);
1735
+ const suspectedVars = [...content.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g)]
1736
+ .map((match) => match[1] ?? '')
1737
+ .filter((candidate) => DISCOVERY_GLOBAL_PATTERN.test(candidate));
1718
1738
  return { content, suspectedVars };
1719
1739
  });
1720
1740
  }
@@ -2028,6 +2048,20 @@ function runPageWorldRequest<T>(action: string, payload: Record<string, unknown>
2028
2048
  }
2029
2049
  return segments;
2030
2050
  };
2051
+ const buildPathExpression = (path) => {
2052
+ const segments = parsePath(path);
2053
+ return segments
2054
+ .map((segment, index) => {
2055
+ if (typeof segment === 'number') {
2056
+ return '[' + segment + ']';
2057
+ }
2058
+ if (index === 0) {
2059
+ return segment;
2060
+ }
2061
+ return '.' + segment;
2062
+ })
2063
+ .join('');
2064
+ };
2031
2065
  const readPath = (targetWindow, path) => {
2032
2066
  const segments = parsePath(path);
2033
2067
  let current = targetWindow;
@@ -2039,6 +2073,34 @@ function runPageWorldRequest<T>(action: string, payload: Record<string, unknown>
2039
2073
  }
2040
2074
  return current;
2041
2075
  };
2076
+ const resolveExtractValue = (targetWindow, path, resolver) => {
2077
+ const strategy = resolver === 'globalThis' || resolver === 'lexical' ? resolver : 'auto';
2078
+ const lexicalExpression = buildPathExpression(path);
2079
+ const readLexical = () => {
2080
+ try {
2081
+ return targetWindow.eval(lexicalExpression);
2082
+ } catch (error) {
2083
+ if (error instanceof ReferenceError) {
2084
+ throw { code: 'E_NOT_FOUND', message: 'path not found: ' + path };
2085
+ }
2086
+ throw error;
2087
+ }
2088
+ };
2089
+ if (strategy === 'globalThis') {
2090
+ return { resolver: 'globalThis', value: readPath(targetWindow, path) };
2091
+ }
2092
+ if (strategy === 'lexical') {
2093
+ return { resolver: 'lexical', value: readLexical() };
2094
+ }
2095
+ try {
2096
+ return { resolver: 'globalThis', value: readPath(targetWindow, path) };
2097
+ } catch (error) {
2098
+ if (!error || typeof error !== 'object' || error.code !== 'E_NOT_FOUND') {
2099
+ throw error;
2100
+ }
2101
+ }
2102
+ return { resolver: 'lexical', value: readLexical() };
2103
+ };
2042
2104
  const buildScopeTargets = () => {
2043
2105
  if (payload.scope === 'main') {
2044
2106
  return [{
@@ -2058,7 +2120,7 @@ function runPageWorldRequest<T>(action: string, payload: Record<string, unknown>
2058
2120
  };
2059
2121
  const toResult = async (target) => {
2060
2122
  if (action === 'globals') {
2061
- const keys = Object.keys(target.targetWindow).filter((key) => /(data|table|json|state|store)/i.test(key)).slice(0, 50);
2123
+ const keys = Object.keys(target.targetWindow).filter((key) => ${DISCOVERY_GLOBAL_PATTERN}.test(key)).slice(0, 50);
2062
2124
  return { url: target.url, framePath: target.framePath, value: keys };
2063
2125
  }
2064
2126
  if (action === 'eval') {
@@ -2066,8 +2128,9 @@ function runPageWorldRequest<T>(action: string, payload: Record<string, unknown>
2066
2128
  return { url: target.url, framePath: target.framePath, value: serialized.value, bytes: serialized.bytes };
2067
2129
  }
2068
2130
  if (action === 'extract') {
2069
- const serialized = serializeValue(readPath(target.targetWindow, payload.path), payload.maxBytes);
2070
- return { url: target.url, framePath: target.framePath, value: serialized.value, bytes: serialized.bytes };
2131
+ const extracted = resolveExtractValue(target.targetWindow, payload.path, payload.resolver);
2132
+ const serialized = serializeValue(extracted.value, payload.maxBytes);
2133
+ return { url: target.url, framePath: target.framePath, value: serialized.value, bytes: serialized.bytes, resolver: extracted.resolver };
2071
2134
  }
2072
2135
  if (action === 'fetch') {
2073
2136
  const headers = { ...(payload.headers || {}) };
@@ -2193,6 +2256,7 @@ function pageEval(expr: string, params: Record<string, unknown>): Promise<{ scop
2193
2256
  function pageExtract(path: string, params: Record<string, unknown>): Promise<{ scope: PageExecutionScope; result?: PageFrameResult<unknown>; results?: Array<PageFrameResult<unknown>> }> {
2194
2257
  return runPageWorldRequest('extract', {
2195
2258
  path,
2259
+ resolver: typeof params.resolver === 'string' ? params.resolver : undefined,
2196
2260
  scope: resolveScope(params),
2197
2261
  maxBytes: typeof params.maxBytes === 'number' ? params.maxBytes : undefined,
2198
2262
  framePath: pageWorldFramePath()
@@ -2216,7 +2280,7 @@ function pageFetch(params: Record<string, unknown>): Promise<{ scope: PageExecut
2216
2280
 
2217
2281
  async function globalsPreview(): Promise<string[]> {
2218
2282
  return Object.keys(window)
2219
- .filter((key) => /(data|table|json|state|store)/i.test(key))
2283
+ .filter((key) => DISCOVERY_GLOBAL_PATTERN.test(key))
2220
2284
  .slice(0, 50);
2221
2285
  }
2222
2286
 
@@ -2230,23 +2294,30 @@ async function collectInspectionState(params: Record<string, unknown> = {}): Pro
2230
2294
  const visibleText = pageTextChunks(root, 40, 320);
2231
2295
  const combinedText = visibleText.map((chunk) => chunk.text).join('\n');
2232
2296
  const scripts = listInlineScripts();
2233
- const visibleTimestamps = timestampCandidatesFromText(combinedText, Array.isArray(params.patterns) ? params.patterns.map(String) : undefined);
2234
- const inlineTimestamps = timestampCandidatesFromText(
2297
+ const visibleTimestampCandidates = timestampCandidateMatchesFromText(
2298
+ combinedText,
2299
+ Array.isArray(params.patterns) ? params.patterns.map(String) : undefined
2300
+ );
2301
+ const inlineTimestampCandidates = timestampCandidateMatchesFromText(
2235
2302
  scripts.map((script) => script.content).join('\n'),
2236
2303
  Array.isArray(params.patterns) ? params.patterns.map(String) : undefined
2237
2304
  );
2305
+ const previewGlobals = await globalsPreview();
2306
+ const suspiciousGlobals = [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50);
2238
2307
  return {
2239
2308
  url: metadata.url,
2240
2309
  title: metadata.title,
2241
2310
  html: redactHtmlSnapshot(document.documentElement),
2242
2311
  visibleText,
2243
- visibleTimestamps,
2244
- inlineTimestamps,
2245
- suspiciousGlobals: [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50),
2246
- globalsPreview: await globalsPreview(),
2312
+ visibleTimestamps: visibleTimestampCandidates.map((candidate) => candidate.value),
2313
+ visibleTimestampCandidates,
2314
+ inlineTimestamps: inlineTimestampCandidates.map((candidate) => candidate.value),
2315
+ inlineTimestampCandidates,
2316
+ suspiciousGlobals,
2317
+ globalsPreview: previewGlobals,
2247
2318
  scripts: {
2248
2319
  inlineCount: scripts.length,
2249
- suspectedDataVars: [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50)
2320
+ suspectedDataVars: suspiciousGlobals
2250
2321
  },
2251
2322
  storage: storageMetadata(),
2252
2323
  cookies: cookieMetadata(),
@@ -446,7 +446,7 @@ export function exportHar(tabId: number, limit = MAX_ENTRIES): Record<string, un
446
446
  version: '1.2',
447
447
  creator: {
448
448
  name: 'bak',
449
- version: '0.6.0'
449
+ version: '0.6.1'
450
450
  },
451
451
  entries: entries.map((entry) => ({
452
452
  startedDateTime: new Date(entry.startedAt ?? entry.ts).toISOString(),
@@ -1,68 +1,50 @@
1
- import type { WorkspaceRecord } from './workspace.js';
2
-
3
- export const STORAGE_KEY_SESSION_BINDINGS = 'sessionBindings';
4
- export const LEGACY_STORAGE_KEY_WORKSPACES = 'agentWorkspaces';
5
- export const LEGACY_STORAGE_KEY_WORKSPACE = 'agentWorkspace';
6
-
7
- function isWorkspaceRecord(value: unknown): value is WorkspaceRecord {
8
- if (typeof value !== 'object' || value === null) {
9
- return false;
10
- }
11
- const candidate = value as Record<string, unknown>;
12
- return (
13
- typeof candidate.id === 'string' &&
14
- Array.isArray(candidate.tabIds) &&
15
- (typeof candidate.windowId === 'number' || candidate.windowId === null) &&
16
- (typeof candidate.groupId === 'number' || candidate.groupId === null) &&
17
- (typeof candidate.activeTabId === 'number' || candidate.activeTabId === null) &&
18
- (typeof candidate.primaryTabId === 'number' || candidate.primaryTabId === null)
19
- );
20
- }
21
-
22
- function cloneWorkspaceRecord(state: WorkspaceRecord): WorkspaceRecord {
23
- return {
24
- ...state,
25
- tabIds: [...state.tabIds]
26
- };
27
- }
28
-
29
- function normalizeWorkspaceRecordMap(value: unknown): { found: boolean; map: Record<string, WorkspaceRecord> } {
30
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
31
- return {
32
- found: false,
33
- map: {}
34
- };
35
- }
36
- const normalizedEntries: Array<readonly [string, WorkspaceRecord]> = [];
37
- for (const [workspaceId, entry] of Object.entries(value as Record<string, unknown>)) {
38
- if (!isWorkspaceRecord(entry)) {
39
- continue;
40
- }
41
- normalizedEntries.push([workspaceId, cloneWorkspaceRecord(entry)] as const);
42
- }
43
- return {
44
- found: true,
45
- map: Object.fromEntries(normalizedEntries)
46
- };
47
- }
48
-
49
- export function resolveSessionBindingStateMap(stored: Record<string, unknown>): Record<string, WorkspaceRecord> {
50
- const current = normalizeWorkspaceRecordMap(stored[STORAGE_KEY_SESSION_BINDINGS]);
51
- if (current.found) {
52
- return current.map;
53
- }
54
-
55
- const legacyMap = normalizeWorkspaceRecordMap(stored[LEGACY_STORAGE_KEY_WORKSPACES]);
56
- if (legacyMap.found) {
57
- return legacyMap.map;
58
- }
59
-
60
- const legacySingle = stored[LEGACY_STORAGE_KEY_WORKSPACE];
61
- if (isWorkspaceRecord(legacySingle)) {
62
- return {
63
- [legacySingle.id]: cloneWorkspaceRecord(legacySingle)
64
- };
65
- }
66
-
67
- return {};
68
- }
1
+ import type { SessionBindingRecord } from './session-binding.js';
2
+
3
+ export const STORAGE_KEY_SESSION_BINDINGS = 'sessionBindings';
4
+
5
+ function isSessionBindingRecord(value: unknown): value is SessionBindingRecord {
6
+ if (typeof value !== 'object' || value === null) {
7
+ return false;
8
+ }
9
+ const candidate = value as Record<string, unknown>;
10
+ return (
11
+ typeof candidate.id === 'string' &&
12
+ Array.isArray(candidate.tabIds) &&
13
+ (typeof candidate.windowId === 'number' || candidate.windowId === null) &&
14
+ (typeof candidate.groupId === 'number' || candidate.groupId === null) &&
15
+ (typeof candidate.activeTabId === 'number' || candidate.activeTabId === null) &&
16
+ (typeof candidate.primaryTabId === 'number' || candidate.primaryTabId === null)
17
+ );
18
+ }
19
+
20
+ function cloneSessionBindingRecord(state: SessionBindingRecord): SessionBindingRecord {
21
+ return {
22
+ ...state,
23
+ tabIds: [...state.tabIds]
24
+ };
25
+ }
26
+
27
+ function normalizeSessionBindingRecordMap(value: unknown): { found: boolean; map: Record<string, SessionBindingRecord> } {
28
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
29
+ return {
30
+ found: false,
31
+ map: {}
32
+ };
33
+ }
34
+ const normalizedEntries: Array<readonly [string, SessionBindingRecord]> = [];
35
+ for (const [bindingId, entry] of Object.entries(value as Record<string, unknown>)) {
36
+ if (!isSessionBindingRecord(entry)) {
37
+ continue;
38
+ }
39
+ normalizedEntries.push([bindingId, cloneSessionBindingRecord(entry)] as const);
40
+ }
41
+ return {
42
+ found: true,
43
+ map: Object.fromEntries(normalizedEntries)
44
+ };
45
+ }
46
+
47
+ export function resolveSessionBindingStateMap(stored: Record<string, unknown>): Record<string, SessionBindingRecord> {
48
+ const current = normalizeSessionBindingRecordMap(stored[STORAGE_KEY_SESSION_BINDINGS]);
49
+ return current.found ? current.map : {};
50
+ }