getraw 0.2.0 → 0.3.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,798 @@
1
+ import { parseScript } from "meriyah";
2
+
3
+ type ESTreeNode = Record<string, unknown> & { type: string; range?: [number, number] };
4
+
5
+ const WALK_STOP = Symbol("WALK_STOP");
6
+
7
+ const JS_BUILTINS = new Set([
8
+ "AbortController", "AbortSignal", "Array", "ArrayBuffer", "AsyncContext", "Atomics",
9
+ "AudioContext", "BigInt", "BigInt64Array", "BigUint64Array", "Blob", "Boolean",
10
+ "BroadcastChannel", "Buffer", "CanvasRenderingContext2D", "clearImmediate", "clearInterval",
11
+ "clearTimeout", "confirm", "console", "Crypto", "CustomEvent", "DataView", "Date",
12
+ "decodeURI", "decodeURIComponent", "document", "Element", "encodeURI", "encodeURIComponent",
13
+ "Error", "escape", "eval", "Event", "EventTarget", "fetch", "File", "FileReader",
14
+ "Float32Array", "Float64Array", "FormData", "function", "global", "globalThis",
15
+ "hasOwnProperty", "Headers", "History", "HTMLElement", "HTMLCollection", "IDBKeyRange",
16
+ "Infinity", "Int16Array", "Int32Array", "Int8Array", "Intl", "IntersectionObserver",
17
+ "isFinite", "isNaN", "isPrototypeOf", "JSON", "location", "log", "Map", "Math",
18
+ "MediaRecorder", "MediaSource", "MediaStream", "MemberExpression", "MutationObserver",
19
+ "NaN", "navigator", "Node", "NodeList", "Number", "Object", "OfflineAudioContext",
20
+ "parse", "parseFloat", "parseInt", "Performance", "process", "Promise", "prompt",
21
+ "prototype", "Proxy", "ReadableStream", "Reflect", "RegExp", "requestAnimationFrame",
22
+ "requestIdleCallback", "Request", "Response", "ResizeObserver", "Screen", "setImmediate",
23
+ "setInterval", "setTimeout", "SharedArrayBuffer", "SharedWorker", "SourceBuffer", "split",
24
+ "String", "stringify", "structuredClone", "SubtleCrypto", "Symbol", "TextDecoder",
25
+ "TextEncoder", "this", "toString", "TransformStream", "Uint16Array", "Uint32Array",
26
+ "Uint8Array", "Uint8ClampedArray", "undefined", "unescape", "URL", "URLSearchParams",
27
+ "valueOf", "WeakMap", "WeakSet", "WebAssembly", "WebGLRenderingContext", "window",
28
+ "Worker", "WritableStream", "XMLHttpRequest", "alert", "arguments", "atob", "btoa",
29
+ "cancelAnimationFrame", "cancelIdleCallback", "queueMicrotask", "self",
30
+ ]);
31
+
32
+ const INDENT = " ";
33
+
34
+ interface ExtractionConfig {
35
+ friendlyName: string;
36
+ match: (node: ESTreeNode) => ESTreeNode | false;
37
+ collectDependencies?: boolean;
38
+ stopWhenReady?: boolean;
39
+ }
40
+
41
+ interface VariableMetadata {
42
+ name: string;
43
+ node: ESTreeNode;
44
+ dependents: Set<string>;
45
+ dependencies: Set<string>;
46
+ predeclared: boolean;
47
+ prototypeAliases: Map<string, Set<VariableMetadata>>;
48
+ }
49
+
50
+ interface ExtractionState {
51
+ config: Required<Pick<ExtractionConfig, "friendlyName" | "match" | "collectDependencies" | "stopWhenReady">>;
52
+ dependencies: Set<string>;
53
+ dependents: Set<string>;
54
+ ready: boolean;
55
+ node?: ESTreeNode;
56
+ metadata?: VariableMetadata;
57
+ matchContext?: ESTreeNode;
58
+ }
59
+
60
+ function walkAst(
61
+ root: ESTreeNode,
62
+ visitor: { enter?: (node: ESTreeNode, parent: ESTreeNode | null) => unknown; leave?: (node: ESTreeNode, parent: ESTreeNode | null) => unknown } | ((node: ESTreeNode, parent: ESTreeNode | null) => unknown),
63
+ ): void {
64
+ if (!root || typeof root !== "object") return;
65
+ const stack: Array<{ node: ESTreeNode; parent: ESTreeNode | null; exit: boolean }> = [{ node: root, parent: null, exit: false }];
66
+ const ancestors: ESTreeNode[] = [];
67
+ const enter = typeof visitor === "function" ? visitor : visitor.enter ?? null;
68
+ const leave = typeof visitor === "function" ? null : visitor.leave ?? null;
69
+ let shouldStop = false;
70
+
71
+ while (!shouldStop && stack.length > 0) {
72
+ const frame = stack.pop()!;
73
+ const { node, parent, exit } = frame;
74
+ if (exit) {
75
+ ancestors.pop();
76
+ if (leave && leave(node, parent) === WALK_STOP) shouldStop = true;
77
+ continue;
78
+ }
79
+ if (!node || typeof node.type !== "string") continue;
80
+ const result = enter ? enter(node, parent) : undefined;
81
+ if (result === WALK_STOP) { shouldStop = true; continue; }
82
+ if (result === true) continue;
83
+ stack.push({ node, parent, exit: true });
84
+ ancestors.push(node);
85
+ for (const key in node) {
86
+ if (key === "loc" || key === "range" || key === "start" || key === "end") continue;
87
+ if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
88
+ const value = node[key];
89
+ if (!value) continue;
90
+ if (Array.isArray(value)) {
91
+ for (let i = value.length - 1; i >= 0; i--) {
92
+ const item = value[i] as ESTreeNode;
93
+ if (item && typeof item.type === "string") {
94
+ stack.push({ node: item, parent: node, exit: false });
95
+ }
96
+ }
97
+ } else if (typeof value === "object" && typeof (value as ESTreeNode).type === "string") {
98
+ stack.push({ node: value as ESTreeNode, parent: node, exit: false });
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ function extractNodeSource(node: ESTreeNode, source: string): string | null {
105
+ const range = node.range;
106
+ return range ? source.slice(range[0], range[1]) : null;
107
+ }
108
+
109
+ function memberToString(expr: ESTreeNode, source: string): string | null {
110
+ if (expr.type !== "MemberExpression") return null;
111
+ const segments: string[] = [];
112
+ let cur: ESTreeNode | null = expr;
113
+ while (cur && cur.type === "MemberExpression") {
114
+ const prop = cur.property as ESTreeNode;
115
+ if (!prop) return null;
116
+ if (cur.computed) {
117
+ const propSource = extractNodeSource(prop, source);
118
+ if (!propSource) return null;
119
+ segments.unshift(`[${propSource.trim()}]`);
120
+ } else {
121
+ if (prop.type !== "Identifier") return null;
122
+ segments.unshift(`.${prop.name as string}`);
123
+ }
124
+ cur = cur.object as ESTreeNode;
125
+ }
126
+ let base: string | null = null;
127
+ if (cur?.type === "Identifier") base = cur.name as string;
128
+ else if (cur?.type === "ThisExpression") base = "this";
129
+ return base ? base + segments.join("") : null;
130
+ }
131
+
132
+ function memberBaseName(expr: ESTreeNode, source: string): string | null {
133
+ let target = (expr as Record<string, ESTreeNode>).object;
134
+ while (target && target.type === "MemberExpression") {
135
+ const parentName = memberToString(target, source);
136
+ if (parentName) return parentName;
137
+ target = target.object as ESTreeNode;
138
+ }
139
+ if (target?.type === "Identifier") return target.name as string;
140
+ if (target?.type === "ThisExpression") return "this";
141
+ return null;
142
+ }
143
+
144
+ function nsigMatcher(node: ESTreeNode): ESTreeNode | false {
145
+ if (node.type !== "VariableDeclarator") return false;
146
+ const init = node.init as ESTreeNode | null;
147
+ if (!init || init.type !== "FunctionExpression") return false;
148
+ const params = init.params as ESTreeNode[];
149
+ if (params.length < 3) return false;
150
+ const [url, sigName, sigValue] = params;
151
+ if (url.type !== "Identifier" || sigName.type !== "AssignmentPattern" || sigValue.type !== "AssignmentPattern") return false;
152
+ const body = init.body as ESTreeNode;
153
+ const blockBody = (body?.body ?? []) as ESTreeNode[];
154
+ let hasUrlCtor = false;
155
+ let hasSetAlr = false;
156
+ for (const statement of blockBody) {
157
+ if (statement.type !== "ExpressionStatement") continue;
158
+ const expr = statement.expression as ESTreeNode;
159
+ if (expr.type === "AssignmentExpression" && expr.operator === "=" &&
160
+ (expr.left as ESTreeNode).type === "Identifier" && (expr.left as ESTreeNode).name === (url.name as string)) {
161
+ const right = expr.right as ESTreeNode;
162
+ if (right.type === "NewExpression" && (right.callee as ESTreeNode).type === "MemberExpression") hasUrlCtor = true;
163
+ }
164
+ if (expr.type === "CallExpression" && (expr.callee as ESTreeNode).type === "MemberExpression") {
165
+ const args = expr.arguments as ESTreeNode[];
166
+ if (args.length === 2 && args[0].type === "Literal" && args[0].value === "alr" &&
167
+ args[1].type === "Literal" && args[1].value === "yes") hasSetAlr = true;
168
+ }
169
+ }
170
+ if (!hasUrlCtor || !hasSetAlr) return false;
171
+ return node;
172
+ }
173
+
174
+ function timestampMatcher(node: ESTreeNode): ESTreeNode | false {
175
+ if (node.type !== "VariableDeclarator" || (node.init as ESTreeNode | null)?.type !== "FunctionExpression") return false;
176
+ const funcBody = (node.init as ESTreeNode).body as ESTreeNode;
177
+ if (!funcBody) return false;
178
+ let foundObject: ESTreeNode | null = null;
179
+ walkAst(funcBody, (innerNode: ESTreeNode) => {
180
+ if (innerNode.type === "ObjectExpression") {
181
+ for (const prop of (innerNode.properties as ESTreeNode[])) {
182
+ if (prop.type === "Property" && (prop.key as ESTreeNode).type === "Identifier" &&
183
+ (prop.key as ESTreeNode).name === "signatureTimestamp") {
184
+ foundObject = prop;
185
+ return WALK_STOP;
186
+ }
187
+ }
188
+ }
189
+ });
190
+ return foundObject || false;
191
+ }
192
+
193
+ class JsAnalyzer {
194
+ source: string;
195
+ programAst: ESTreeNode;
196
+ hasExtractions: boolean;
197
+ extractionStates: ExtractionState[];
198
+ dependentsTracker = new Map<string, Set<string>>();
199
+ pendingPrototypeAliasBinding: [string, VariableMetadata] | null = null;
200
+ iifeParamName: string | null = null;
201
+ declaredVariables = new Map<string, VariableMetadata>();
202
+
203
+ constructor(code: string, options: { extractions?: ExtractionConfig[] } = {}) {
204
+ this.source = code;
205
+ const configs = options.extractions ?? [];
206
+ this.extractionStates = configs.map((config) => ({
207
+ config: { collectDependencies: true, stopWhenReady: true, ...config },
208
+ dependencies: new Set<string>(),
209
+ dependents: new Set<string>(),
210
+ ready: false,
211
+ }));
212
+ this.hasExtractions = this.extractionStates.length > 0;
213
+ this.programAst = parseScript(code, { ranges: true, loc: false, module: false }) as unknown as ESTreeNode;
214
+ this.analyzeAst();
215
+ }
216
+
217
+ private analyzeAst(): void {
218
+ let iifeBody: ESTreeNode | undefined;
219
+ for (const statement of (this.programAst.body as ESTreeNode[])) {
220
+ if (statement.type === "ExpressionStatement" && (statement.expression as ESTreeNode).type === "CallExpression") {
221
+ const callExpr = statement.expression as ESTreeNode;
222
+ if ((callExpr.callee as ESTreeNode).type === "FunctionExpression") {
223
+ const funcExpr = callExpr.callee as ESTreeNode;
224
+ const firstParam = ((funcExpr.params as ESTreeNode[]).length > 0 ? (funcExpr.params as ESTreeNode[])[0] : null);
225
+ if (!this.iifeParamName && firstParam?.type === "Identifier") {
226
+ this.iifeParamName = firstParam.name as string;
227
+ }
228
+ if ((funcExpr.body as ESTreeNode)?.type === "BlockStatement") {
229
+ iifeBody = funcExpr.body as ESTreeNode;
230
+ break;
231
+ }
232
+ }
233
+ }
234
+ }
235
+ if (!iifeBody) return;
236
+
237
+ for (const currentNode of (iifeBody.body as ESTreeNode[])) {
238
+ switch (currentNode.type) {
239
+ case "ExpressionStatement": {
240
+ const assignment = currentNode.expression as ESTreeNode;
241
+ if (assignment.type !== "AssignmentExpression") continue;
242
+ const left = assignment.left as ESTreeNode;
243
+ const right = assignment.right as ESTreeNode;
244
+
245
+ if (right.type === "MemberExpression" && !right.computed &&
246
+ (right.property as ESTreeNode).type === "Identifier" && (right.property as ESTreeNode).name === "prototype") {
247
+ const protoSource = memberToString(right, this.source);
248
+ const aliasTarget = left.type === "Identifier" ? left.name as string : memberToString(left, this.source);
249
+ if (protoSource) {
250
+ const ownerMeta = this.declaredVariables.get(protoSource.replace(".prototype", ""));
251
+ if (aliasTarget && ownerMeta) {
252
+ const aliasExpr = `${aliasTarget}.`;
253
+ this.pendingPrototypeAliasBinding = [aliasExpr, ownerMeta];
254
+ ownerMeta.prototypeAliases.set(aliasExpr, new Set());
255
+ }
256
+ }
257
+ }
258
+
259
+ if (left.type === "Identifier") {
260
+ const existing = this.declaredVariables.get(left.name as string);
261
+ if (!existing) continue;
262
+ (existing.node as Record<string, unknown>).init = right;
263
+ if (this.needsDeps(right)) existing.dependencies = this.findDependencies(right, left.name as string);
264
+ if (this.onMatch(existing.node, existing)) return;
265
+ } else if (left.type === "MemberExpression") {
266
+ const memberName = memberToString(left, this.source);
267
+ const activeAlias = this.pendingPrototypeAliasBinding?.[0];
268
+
269
+ if (activeAlias && (memberName?.includes(activeAlias) || memberName === activeAlias.slice(0, -1))) {
270
+ const ownerMeta = this.declaredVariables.get(this.pendingPrototypeAliasBinding?.[1].name || "");
271
+ if (ownerMeta) {
272
+ const existing = ownerMeta.prototypeAliases.get(activeAlias);
273
+ const meta: VariableMetadata = {
274
+ name: memberName!, node: currentNode, dependents: this.dependentsTracker.get(memberName!) || new Set(),
275
+ predeclared: false, prototypeAliases: new Map(), dependencies: this.findDependencies(right, memberName!),
276
+ };
277
+ if (existing) existing.add(meta);
278
+ else ownerMeta.prototypeAliases.set(activeAlias, new Set([meta]));
279
+ }
280
+ } else {
281
+ this.pendingPrototypeAliasBinding = null;
282
+ }
283
+
284
+ if (!memberName || this.declaredVariables.has(memberName)) continue;
285
+ const metadata: VariableMetadata = {
286
+ name: memberName, node: currentNode, dependents: this.dependentsTracker.get(memberName) || new Set(),
287
+ predeclared: false, prototypeAliases: new Map(), dependencies: this.findDependencies(right, memberName),
288
+ };
289
+ const baseName = memberBaseName(left, this.source);
290
+ if (baseName && baseName !== memberName && !baseName.startsWith("this.")) {
291
+ metadata.dependencies.add(baseName.replace(".prototype", ""));
292
+ }
293
+ if (this.dependentsTracker.has(memberName)) this.dependentsTracker.delete(memberName);
294
+ this.declaredVariables.set(memberName, metadata);
295
+ if (this.onMatch(currentNode, metadata)) return;
296
+ }
297
+ break;
298
+ }
299
+ case "VariableDeclaration": {
300
+ this.pendingPrototypeAliasBinding = null;
301
+ for (const decl of (currentNode.declarations as ESTreeNode[])) {
302
+ if ((decl.id as ESTreeNode).type !== "Identifier") continue;
303
+ const name = (decl.id as ESTreeNode).name as string;
304
+ const metadata: VariableMetadata = {
305
+ name, node: decl, dependents: this.dependentsTracker.get(name) || new Set(),
306
+ prototypeAliases: new Map(), dependencies: new Set(), predeclared: false,
307
+ };
308
+ const init = decl.init as ESTreeNode | null;
309
+ if (!init && currentNode.kind === "var") metadata.predeclared = true;
310
+ else if (init && this.needsDeps(init)) metadata.dependencies = this.findDependencies(init, name);
311
+ if (this.dependentsTracker.has(name)) this.dependentsTracker.delete(name);
312
+ this.declaredVariables.set(name, metadata);
313
+ if (this.onMatch(decl, metadata)) return;
314
+ }
315
+ break;
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ private needsDeps(node: ESTreeNode): boolean {
322
+ if (!node) return false;
323
+ return ["FunctionExpression", "ArrowFunctionExpression", "ArrayExpression", "LogicalExpression",
324
+ "CallExpression", "NewExpression", "MemberExpression", "BinaryExpression",
325
+ "ConditionalExpression", "ObjectExpression", "SequenceExpression", "ClassExpression", "Identifier"].includes(node.type);
326
+ }
327
+
328
+ private onMatch(node: ESTreeNode, metadata: VariableMetadata): boolean {
329
+ if (!this.hasExtractions) return false;
330
+ let matched = false;
331
+ for (const state of this.extractionStates) {
332
+ if (!state.node) {
333
+ if (node.type === "VariableDeclarator" && !(node as Record<string, unknown>).init) continue;
334
+ const result = state.config.match(node);
335
+ if (!result) continue;
336
+ state.node = node;
337
+ matched = true;
338
+ state.metadata = metadata;
339
+ state.dependents = metadata.dependents;
340
+ state.dependencies = metadata.dependencies;
341
+ if (typeof result !== "boolean") state.matchContext = result;
342
+ this.refreshState(state);
343
+ } else if (state.node !== node) {
344
+ this.refreshState(state);
345
+ if (this.shouldStop()) return true;
346
+ }
347
+ }
348
+ if (!matched) return false;
349
+ return this.shouldStop();
350
+ }
351
+
352
+ private refreshState(state: ExtractionState): void {
353
+ if (!state.node) { state.ready = false; return; }
354
+ if (state.config.collectDependencies === false) { state.ready = true; return; }
355
+ if (!state.metadata) { state.ready = false; return; }
356
+ state.ready = this.areDepsResolved(state.dependencies);
357
+ }
358
+
359
+ private shouldStop(): boolean {
360
+ if (!this.hasExtractions) return false;
361
+ let has = false;
362
+ for (const state of this.extractionStates) {
363
+ if (state.config.stopWhenReady === false) continue;
364
+ has = true;
365
+ if (!state.node || !state.ready) return false;
366
+ }
367
+ return has;
368
+ }
369
+
370
+ private areDepsResolved(deps: Set<string>, seen = new Set<string>()): boolean {
371
+ for (const dep of deps) {
372
+ if (!dep || JS_BUILTINS.has(dep) || dep === this.iifeParamName) continue;
373
+ if (seen.has(dep)) continue;
374
+ const meta = this.declaredVariables.get(dep);
375
+ if (!meta) return false;
376
+ seen.add(dep);
377
+ if (!this.areDepsResolved(meta.dependencies, seen)) return false;
378
+ }
379
+ return true;
380
+ }
381
+
382
+ findDependencies(rootNode: ESTreeNode, identifierName: string): Set<string> {
383
+ const dependencies = new Set<string>();
384
+ if (!rootNode) return dependencies;
385
+ const scopeStack: Array<{ names: Set<string>; type: string }> = [{ names: new Set(), type: "block" }];
386
+ const currentScope = () => scopeStack[scopeStack.length - 1];
387
+ const isInScope = (name: string) => {
388
+ for (let i = scopeStack.length - 1; i >= 0; i--) { if (scopeStack[i].names.has(name)) return true; }
389
+ return false;
390
+ };
391
+ const rootIdName = "id" in rootNode && (rootNode.id as ESTreeNode | null)?.type === "Identifier"
392
+ ? (rootNode.id as ESTreeNode).name as string : undefined;
393
+
394
+ const collectBindings = (pattern: ESTreeNode | null, target: Set<string>) => {
395
+ if (!pattern) return;
396
+ switch (pattern.type) {
397
+ case "Identifier": target.add(pattern.name as string); break;
398
+ case "ObjectPattern":
399
+ for (const prop of (pattern.properties as ESTreeNode[])) {
400
+ if (prop.type === "RestElement") collectBindings(prop.argument as ESTreeNode, target);
401
+ else if (prop.type === "Property") collectBindings(prop.value as ESTreeNode, target);
402
+ }
403
+ break;
404
+ case "ArrayPattern":
405
+ for (const el of (pattern.elements as (ESTreeNode | null)[])) { if (el) collectBindings(el, target); }
406
+ break;
407
+ case "RestElement": collectBindings(pattern.argument as ESTreeNode, target); break;
408
+ case "AssignmentPattern": collectBindings(pattern.left as ESTreeNode, target); break;
409
+ }
410
+ };
411
+
412
+ const collectParams = (fn: ESTreeNode, target: Set<string>) => {
413
+ if (!fn?.params) return;
414
+ for (const p of fn.params as ESTreeNode[]) collectBindings(p, target);
415
+ };
416
+
417
+ walkAst(rootNode, {
418
+ enter: (n: ESTreeNode, parent: ESTreeNode | null) => {
419
+ switch (n.type) {
420
+ case "FunctionDeclaration":
421
+ case "FunctionExpression":
422
+ case "ArrowFunctionExpression": {
423
+ const fnName = "id" in n ? (n.id as ESTreeNode | null)?.name as string | undefined : undefined;
424
+ if (n.type === "FunctionDeclaration" && fnName) currentScope().names.add(fnName);
425
+ const fnScope = { names: new Set<string>(), type: "function" };
426
+ if (n.type === "FunctionExpression" && fnName) fnScope.names.add(fnName);
427
+ collectParams(n, fnScope.names);
428
+ scopeStack.push(fnScope);
429
+ break;
430
+ }
431
+ case "BlockStatement": scopeStack.push({ names: new Set(), type: "block" }); break;
432
+ case "CatchClause": {
433
+ const s = new Set<string>();
434
+ if (n.param) collectBindings(n.param as ESTreeNode, s);
435
+ scopeStack.push({ names: s, type: "block" });
436
+ break;
437
+ }
438
+ case "VariableDeclaration": {
439
+ const targetScope = (n.kind as string) === "var"
440
+ ? (scopeStack.findLast((s) => s.type === "function") ?? currentScope())
441
+ : currentScope();
442
+ for (const d of n.declarations as ESTreeNode[]) collectBindings(d.id as ESTreeNode, targetScope.names);
443
+ break;
444
+ }
445
+ case "ClassDeclaration": {
446
+ if ((n.id as ESTreeNode | null)?.name) currentScope().names.add((n.id as ESTreeNode).name as string);
447
+ break;
448
+ }
449
+ case "LabeledStatement": {
450
+ if ((n.label as ESTreeNode | null)?.type === "Identifier") currentScope().names.add((n.label as ESTreeNode).name as string);
451
+ break;
452
+ }
453
+ case "Identifier": {
454
+ if ((n.name as string) === rootIdName) return;
455
+ if (parent?.type === "Property" && parent.key === n && !parent.computed) return;
456
+ if (parent?.type === "MethodDefinition" && parent.key === n && !parent.computed) return;
457
+ if (parent?.type === "MemberExpression" && parent.property === n && !parent.computed) {
458
+ if ((parent.object as ESTreeNode).type === "ThisExpression") return;
459
+ const full = memberToString(parent, this.source);
460
+ if (!full) return;
461
+ const declVar = this.declaredVariables.get(full);
462
+ if (declVar) { declVar.dependents.add(identifierName); dependencies.add(full); }
463
+ else if ((parent.object as ESTreeNode).type === "Identifier") {
464
+ const baseName = (parent.object as ESTreeNode).name as string;
465
+ const declBase = this.declaredVariables.get(baseName);
466
+ if ((declBase || baseName === this.iifeParamName) && !isInScope(baseName) && !JS_BUILTINS.has(baseName)) {
467
+ declBase?.dependents.add(identifierName);
468
+ dependencies.add(full);
469
+ const existing = this.dependentsTracker.get(full);
470
+ if (existing) existing.add(identifierName);
471
+ else this.dependentsTracker.set(full, new Set([identifierName]));
472
+ }
473
+ }
474
+ return;
475
+ }
476
+ if (parent?.type === "MetaProperty") return;
477
+ if (isInScope(n.name as string) || JS_BUILTINS.has(n.name as string)) return;
478
+ dependencies.add(n.name as string);
479
+ const declVar = this.declaredVariables.get(n.name as string);
480
+ if (declVar) declVar.dependents.add(identifierName);
481
+ else {
482
+ const existing = this.dependentsTracker.get(n.name as string);
483
+ if (existing) existing.add(identifierName);
484
+ else this.dependentsTracker.set(n.name as string, new Set([identifierName]));
485
+ }
486
+ break;
487
+ }
488
+ case "ForStatement":
489
+ case "ForInStatement":
490
+ case "ForOfStatement":
491
+ scopeStack.push({ names: new Set(), type: "block" }); break;
492
+ }
493
+ },
494
+ leave: (n: ESTreeNode) => {
495
+ switch (n.type) {
496
+ case "FunctionDeclaration": case "FunctionExpression": case "ArrowFunctionExpression":
497
+ case "BlockStatement": case "CatchClause":
498
+ case "ForStatement": case "ForInStatement": case "ForOfStatement":
499
+ if (scopeStack.length > 1) scopeStack.pop();
500
+ break;
501
+ }
502
+ },
503
+ });
504
+ return dependencies;
505
+ }
506
+
507
+ getExtractedMatches(): ExtractionState[] {
508
+ return this.extractionStates.filter((s) => !!s.node);
509
+ }
510
+
511
+ getSource(): string { return this.source; }
512
+ }
513
+
514
+ function isSafeInitializer(node: ESTreeNode | null, analyzer: JsAnalyzer, mode: "strict" | "loose" = "strict"): boolean {
515
+ if (!node) return true;
516
+ switch (node.type) {
517
+ case "ClassExpression": return true;
518
+ case "Literal": {
519
+ const v = node.value;
520
+ return typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null || !!node.regex;
521
+ }
522
+ case "TemplateLiteral":
523
+ return (node.expressions as ESTreeNode[]).every((e) => isSafeInitializer(e, analyzer, mode));
524
+ case "ArrayExpression":
525
+ return (node.elements as (ESTreeNode | null)[]).every((e) => {
526
+ if (!e) return true;
527
+ if (e.type === "SpreadElement") return false;
528
+ return isSafeInitializer(e, analyzer, mode);
529
+ });
530
+ case "ObjectExpression":
531
+ return (node.properties as ESTreeNode[]).every((p) => {
532
+ if (p.type !== "Property" || p.computed || p.kind !== "init") return false;
533
+ const v = p.value as ESTreeNode;
534
+ return v.type === "FunctionExpression" || v.type === "ArrowFunctionExpression" || v.type === "Literal";
535
+ });
536
+ case "CallExpression": {
537
+ if ((node.callee as ESTreeNode).type === "Identifier" && JS_BUILTINS.has((node.callee as ESTreeNode).name as string))
538
+ return (node.arguments as ESTreeNode[]).every((a) => a.type !== "SpreadElement" && isSafeInitializer(a, analyzer, mode));
539
+ if ((node.callee as ESTreeNode).type === "MemberExpression") {
540
+ if (!isSafeInitializer((node.callee as ESTreeNode).object as ESTreeNode, analyzer, mode)) return false;
541
+ if (mode === "strict") {
542
+ const prop = (node.callee as ESTreeNode).property as ESTreeNode;
543
+ if ((node.callee as ESTreeNode).computed || !JS_BUILTINS.has(prop.name as string)) return false;
544
+ }
545
+ return (node.arguments as ESTreeNode[]).every((a) => a.type !== "SpreadElement" && isSafeInitializer(a, analyzer, mode));
546
+ }
547
+ return false;
548
+ }
549
+ case "NewExpression":
550
+ if ((node.callee as ESTreeNode).type === "Identifier" && JS_BUILTINS.has((node.callee as ESTreeNode).name as string))
551
+ return (node.arguments as ESTreeNode[]).every((a) => a.type !== "SpreadElement" && isSafeInitializer(a, analyzer, mode));
552
+ if (mode === "loose") return (node.arguments as ESTreeNode[]).every((a) => a.type !== "SpreadElement" && isSafeInitializer(a, analyzer, mode));
553
+ return false;
554
+ case "UnaryExpression": return isSafeInitializer(node.argument as ESTreeNode, analyzer, mode);
555
+ case "FunctionExpression": case "ArrowFunctionExpression": case "Identifier": return true;
556
+ case "MemberExpression":
557
+ if (mode === "loose") return isSafeInitializer(node.object as ESTreeNode, analyzer, mode);
558
+ if (!node.computed && (node.property as ESTreeNode).type === "Identifier" && (node.property as ESTreeNode).name === "prototype") return true;
559
+ return false;
560
+ case "LogicalExpression": case "BinaryExpression":
561
+ return isSafeInitializer(node.left as ESTreeNode, analyzer, mode) && isSafeInitializer(node.right as ESTreeNode, analyzer, mode);
562
+ case "ConditionalExpression":
563
+ if (mode === "loose") return isSafeInitializer(node.test as ESTreeNode, analyzer, mode) && isSafeInitializer(node.consequent as ESTreeNode, analyzer, mode) && isSafeInitializer(node.alternate as ESTreeNode, analyzer, mode);
564
+ return false;
565
+ case "AssignmentExpression":
566
+ if ((node.left as ESTreeNode).type === "MemberExpression" && !(node.left as ESTreeNode).computed) {
567
+ const obj = (node.left as ESTreeNode).object as ESTreeNode;
568
+ if (obj.type === "Identifier" && analyzer.declaredVariables.get(obj.name as string)?.node.init !== undefined)
569
+ return isSafeInitializer(node.right as ESTreeNode, analyzer, mode);
570
+ } else if ((node.left as ESTreeNode).type === "Identifier") {
571
+ if (analyzer.declaredVariables.has((node.left as ESTreeNode).name as string))
572
+ return isSafeInitializer(node.right as ESTreeNode, analyzer, mode);
573
+ }
574
+ return false;
575
+ default: return false;
576
+ }
577
+ }
578
+
579
+ function getInitFallback(init: ESTreeNode | null): string {
580
+ switch (init?.type) {
581
+ case "ObjectExpression": case "NewExpression": case "MemberExpression": case "LogicalExpression": return "{}";
582
+ case "ArrayExpression": return "[]";
583
+ default: return "undefined";
584
+ }
585
+ }
586
+
587
+ function renderNode(node: ESTreeNode, preDeclared: boolean, analyzer: JsAnalyzer, disallowSideEffects: boolean): string {
588
+ const source = analyzer.getSource();
589
+ const assignment = node.type === "AssignmentExpression" ? node :
590
+ node.type === "ExpressionStatement" && (node.expression as ESTreeNode).type === "AssignmentExpression" ? node.expression as ESTreeNode : null;
591
+ const init = assignment && assignment.operator === "=" ? assignment.right as ESTreeNode :
592
+ node.type === "VariableDeclarator" ? node.init as ESTreeNode | null : null;
593
+ const forceRemove = disallowSideEffects && init && !isSafeInitializer(init, analyzer);
594
+ const fallback = getInitFallback(init);
595
+
596
+ let initSource = fallback;
597
+ if (!forceRemove && init) {
598
+ if (node.type === "VariableDeclarator" && !preDeclared && init.type === "Identifier" && !analyzer.declaredVariables.has(init.name as string)) {
599
+ initSource = fallback;
600
+ } else {
601
+ const left = assignment?.left as ESTreeNode | undefined;
602
+ const isProtoAlias = init?.type === "MemberExpression" && !init.computed && (init.property as ESTreeNode).type === "Identifier" && (init.property as ESTreeNode).name === "prototype";
603
+ if (!isProtoAlias && left?.type === "MemberExpression" && init) {
604
+ if (disallowSideEffects && (left.object as ESTreeNode).type === "Identifier" &&
605
+ init.type !== "FunctionExpression" && init.type !== "ArrowFunctionExpression" &&
606
+ init.type !== "LogicalExpression" && init.type !== "ClassExpression") {
607
+ return `${INDENT}// Skipped ${memberToString(left, source)} assignment.`;
608
+ }
609
+ }
610
+ initSource = extractNodeSource(init, source)?.trim().replace(/;\s*$/, "") || "undefined";
611
+ }
612
+ }
613
+ if (!forceRemove && init && init.type === "SequenceExpression" && !initSource.startsWith("(")) initSource = `(${initSource})`;
614
+
615
+ const idName = node.type === "VariableDeclarator" && (node.id as ESTreeNode).type === "Identifier" ? (node.id as ESTreeNode).name as string :
616
+ assignment && (assignment.left as ESTreeNode).type === "Identifier" ? (assignment.left as ESTreeNode).name as string :
617
+ assignment?.type === "AssignmentExpression" ? memberToString(assignment.left as ESTreeNode, source)?.trim() : "unknown";
618
+
619
+ const assignExpr = `${idName} = ${initSource};`;
620
+ if (node.type === "VariableDeclarator" && node.init && !preDeclared) return `${INDENT}var ${assignExpr}`;
621
+ return `${INDENT}${assignExpr}`;
622
+ }
623
+
624
+ function parseFunctionArguments(analyzer: JsAnalyzer, args: ESTreeNode[]): string[] {
625
+ const params: string[] = [];
626
+ for (const arg of args) {
627
+ if (arg.type === "Identifier" && analyzer.declaredVariables.has(arg.name as string)) params.push(arg.name as string);
628
+ else if (arg.type === "Literal" && (typeof arg.value === "string" || typeof arg.value === "number")) params.push(JSON.stringify(arg.value));
629
+ else if (arg.type === "UnaryExpression") { const s = extractNodeSource(arg, analyzer.getSource()); if (s) params.push(s.trim()); }
630
+ else if (arg.type === "AssignmentPattern" && (arg.left as ESTreeNode).type === "Identifier") params.push((arg.left as ESTreeNode).name as string);
631
+ else if (arg.type === "Identifier") params.push(arg.name as string);
632
+ else if (!params.includes("input")) params.push("input");
633
+ }
634
+ return params;
635
+ }
636
+
637
+ function createWrapperFunction(analyzer: JsAnalyzer, name: string, node: ESTreeNode): string | undefined {
638
+ if (node.type === "CallExpression" && (node.callee as ESTreeNode).type === "Identifier" && analyzer.declaredVariables.has((node.callee as ESTreeNode).name as string)) {
639
+ const target = (node.callee as ESTreeNode).name as string;
640
+ const args = parseFunctionArguments(analyzer, node.arguments as ESTreeNode[]);
641
+ return `${INDENT}function ${name}(${args.join(", ")}) {\n${INDENT}${INDENT}return ${target}(${args.join(", ")});\n${INDENT}}`;
642
+ } else if (node.type === "VariableDeclarator" && (node.init as ESTreeNode | null)?.type === "FunctionExpression" && (node.id as ESTreeNode).type === "Identifier") {
643
+ const target = (node.id as ESTreeNode).name as string;
644
+ const args = parseFunctionArguments(analyzer, (node.init as ESTreeNode).params as ESTreeNode[]);
645
+ return `${INDENT}function ${name}(${args.join(", ")}) {\n${INDENT}${INDENT}return ${target}(${args.join(", ")});\n${INDENT}}`;
646
+ } else if (node.type === "NewExpression" && (node.callee as ESTreeNode).type === "MemberExpression" && ((node.callee as ESTreeNode).object as ESTreeNode).type === "Identifier") {
647
+ const target = memberToString(node.callee as ESTreeNode, analyzer.getSource());
648
+ if (!target) return undefined;
649
+ const args = parseFunctionArguments(analyzer, node.arguments as ESTreeNode[]);
650
+ return `${INDENT}function ${name}(${args.join(", ")}) {\n${INDENT}${INDENT}return new ${target}(${args.join(", ")});\n${INDENT}}`;
651
+ }
652
+ return undefined;
653
+ }
654
+
655
+ function buildScript(analyzer: JsAnalyzer): { output: string; exported: string[]; exportedRawValues?: Record<string, string | null> } {
656
+ const extractions = analyzer.getExtractedMatches();
657
+ const seen = new Set(extractions.map((e) => e.metadata?.name || ""));
658
+ const snippets: string[] = [];
659
+ const predeclaredVars = new Set<string>();
660
+ const exported = new Map<string, ESTreeNode>();
661
+ const exportedRawValues: Record<string, string | null> = {};
662
+
663
+ const visit = (metadata: VariableMetadata | undefined, depth = 0): void => {
664
+ if (!metadata || depth > 100) return;
665
+ for (const dep of metadata.dependencies) {
666
+ if (seen.has(dep)) continue;
667
+ seen.add(dep);
668
+ const depMeta = analyzer.declaredVariables.get(dep);
669
+ if (!depMeta) continue;
670
+ const shouldPredeclare = depMeta.predeclared;
671
+ if (shouldPredeclare && !dep.includes(".")) predeclaredVars.add(dep);
672
+ visit(depMeta, depth + 1);
673
+ snippets.push(renderNode(depMeta.node, shouldPredeclare, analyzer, true));
674
+ if (depMeta.prototypeAliases.size > 0) {
675
+ for (const [, members] of depMeta.prototypeAliases) {
676
+ for (const member of members) { visit(member, depth); snippets.push(renderNode(member.node, shouldPredeclare, analyzer, true)); }
677
+ }
678
+ }
679
+ }
680
+ };
681
+
682
+ for (const extraction of extractions) {
683
+ const fname = extraction.config.friendlyName;
684
+ const skipEmit = fname === "signatureTimestampVar";
685
+ if (!skipEmit) snippets.push(`${INDENT}//#region --- start [${fname}] ---`);
686
+ const shouldPredeclare = extraction.metadata?.predeclared && !skipEmit;
687
+ if (shouldPredeclare && extraction.metadata && !extraction.metadata.name.includes(".")) {
688
+ predeclaredVars.add(extraction.metadata.name);
689
+ }
690
+ if (extraction.config.collectDependencies && !skipEmit) visit(extraction.metadata);
691
+ if (extraction.matchContext && fname) {
692
+ exported.set(fname, extraction.matchContext);
693
+ const ctx = extraction.matchContext;
694
+ if (ctx.type === "Property") exportedRawValues[fname] = extractNodeSource(ctx.value as ESTreeNode, analyzer.getSource());
695
+ else if (ctx.type === "Identifier") exportedRawValues[fname] = ctx.name as string;
696
+ else exportedRawValues[fname] = extractNodeSource(ctx, analyzer.getSource());
697
+ }
698
+ if (!skipEmit) {
699
+ if (extraction.metadata) snippets.push(renderNode(extraction.metadata.node, !!shouldPredeclare, analyzer, true));
700
+ snippets.push(`${INDENT}//#endregion --- end [${fname}] ---\n`);
701
+ }
702
+ }
703
+
704
+ const output: string[] = [];
705
+ output.push("const __jsExtractorGlobal = typeof globalThis !== 'undefined' ? globalThis :");
706
+ output.push(`${INDENT}typeof self !== 'undefined' ? self :`);
707
+ output.push(`${INDENT}typeof window !== 'undefined' ? window :`);
708
+ output.push(`${INDENT}typeof global !== 'undefined' ? global : {};\n`);
709
+ output.push(`const exportedVars = (function(${analyzer.iifeParamName}) {`);
710
+ output.push(`${INDENT}const window = typeof __jsExtractorGlobal.window !== 'undefined' ? __jsExtractorGlobal.window : Object.create(null);`);
711
+ output.push(`${INDENT}const document = typeof __jsExtractorGlobal.document !== 'undefined' ? __jsExtractorGlobal.document : {};`);
712
+ output.push(`${INDENT}const self = typeof __jsExtractorGlobal.self !== 'undefined' ? __jsExtractorGlobal.self : window;\n`);
713
+ if (predeclaredVars.size > 0) output.push(`${INDENT}var ${Array.from(predeclaredVars).join(", ")};\n`);
714
+ output.push(snippets.join("\n"));
715
+
716
+ const exportedVarNames: string[] = [];
717
+ for (const [friendlyName, node] of exported) {
718
+ let funcNode: ESTreeNode | null = null;
719
+ if (node.type === "Identifier") {
720
+ const decl = analyzer.declaredVariables.get(node.name as string);
721
+ if (decl?.node?.type === "VariableDeclarator" && (decl.node.init as ESTreeNode | null)?.type === "FunctionExpression") funcNode = decl.node;
722
+ } else if (["CallExpression", "NewExpression", "VariableDeclarator"].includes(node.type)) funcNode = node;
723
+ if (funcNode) {
724
+ const wrapper = createWrapperFunction(analyzer, friendlyName, funcNode);
725
+ if (wrapper) { output.push(`${wrapper}\n`); exportedVarNames.push(friendlyName); }
726
+ }
727
+ }
728
+
729
+ const rawJson = JSON.stringify(exportedRawValues, null, INDENT.length);
730
+ const rawJsonLines = rawJson.split("\n");
731
+ const formattedRaw = `${rawJsonLines[0]}\n${rawJsonLines.slice(1).map((l) => INDENT + l).join("\n")}`;
732
+ output.push(`${INDENT}const rawValues = ${formattedRaw};\n`);
733
+ exportedVarNames.push("rawValues");
734
+
735
+ output.push(`${INDENT}return { ${exportedVarNames.join(", ")} };`);
736
+ output.push("})({});\n");
737
+
738
+ return { output: output.join("\n"), exported: exportedVarNames, exportedRawValues };
739
+ }
740
+
741
+ export interface PlayerScriptResult {
742
+ output: string;
743
+ signatureTimestamp: number;
744
+ hasNsigFunction: boolean;
745
+ }
746
+
747
+ export function analyzePlayerJs(playerJs: string): PlayerScriptResult {
748
+ const nsigFunctionName = "nsigFunction";
749
+ const timestampVarName = "signatureTimestampVar";
750
+
751
+ const extractions: ExtractionConfig[] = [
752
+ { friendlyName: nsigFunctionName, match: nsigMatcher },
753
+ { friendlyName: timestampVarName, match: timestampMatcher, collectDependencies: false },
754
+ ];
755
+
756
+ const analyzer = new JsAnalyzer(playerJs, { extractions });
757
+ const result = buildScript(analyzer);
758
+
759
+ const sigTs = result.exportedRawValues?.[timestampVarName];
760
+ const signatureTimestamp = sigTs ? parseInt(sigTs, 10) || 0 : 0;
761
+ const hasNsigFunction = result.exported.includes(nsigFunctionName);
762
+
763
+ return {
764
+ output: result.output,
765
+ signatureTimestamp,
766
+ hasNsigFunction,
767
+ };
768
+ }
769
+
770
+ export function getNsigProcessorFn(n?: string, sp?: string, s?: string): string {
771
+ return `function process(n = "", sp = "", s = "") {
772
+ const mockStreamingURL = "https://ytjs.googlevideo.com/videoplayback?expire=1234567890&"+"n="+encodeURIComponent(n);
773
+ const urlCtorFunction = exportedVars.nsigFunction || (() => { throw new Error('No n/sig decipher function extracted') });
774
+ const urlCtor = urlCtorFunction(mockStreamingURL, sp, s);
775
+
776
+ const proto = Object.getPrototypeOf(urlCtor);
777
+ const properties = Object.getOwnPropertyNames(proto);
778
+ const methodBlacklist = ['constructor', 'clone', 'set', 'get'];
779
+
780
+ for (const prop of properties) {
781
+ if (methodBlacklist.includes(prop))
782
+ continue;
783
+
784
+ if (typeof urlCtor[prop] === 'function')
785
+ urlCtor[prop]();
786
+ }
787
+
788
+ const sigResult = urlCtor.get(sp);
789
+ const nResult = urlCtor.get('n');
790
+
791
+ return {
792
+ sig: sigResult ? decodeURIComponent(sigResult) : undefined,
793
+ n: nResult ? decodeURIComponent(nResult) : undefined
794
+ };
795
+ }
796
+
797
+ return process("${n || ""}", "${sp || ""}", "${s || ""}");`;
798
+ }