thespis 0.1.0b1__py3-none-any.whl

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.
thespis/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .stealth import stealth_async, stealth_sync
2
+
3
+ __version__ = "0.1.0-beta"
4
+ __all__ = ["stealth_sync", "stealth_async"]
@@ -0,0 +1,79 @@
1
+ if (!globalThis.chrome) {
2
+ // Basic structure for Chrome
3
+ const installDetails = {
4
+ InstallState: {
5
+ DISABLED: 'disabled',
6
+ INSTALLED: 'installed',
7
+ NOT_INSTALLED: 'not_installed'
8
+ },
9
+ RunningState: {
10
+ CANNOT_RUN: 'cannot_run',
11
+ READY_TO_RUN: 'ready_to_run',
12
+ RUNNING: 'running'
13
+ },
14
+ getDetails: function getDetails() {},
15
+ getIsInstalled: function getIsInstalled() {},
16
+ runningState: function runningState() {}
17
+ };
18
+
19
+ const runtime = {
20
+ OnInstalledReason: {
21
+ CHROME_UPDATE: 'chrome_update',
22
+ INSTALL: 'install',
23
+ SHARED_MODULE_UPDATE: 'shared_module_update',
24
+ UPDATE: 'update'
25
+ },
26
+ OnRestartRequiredReason: {
27
+ APP_UPDATE: 'app_update',
28
+ OS_UPDATE: 'os_update',
29
+ PERIODIC: 'periodic'
30
+ },
31
+ PlatformArch: {
32
+ ARM: 'arm',
33
+ ARM64: 'arm64',
34
+ MIPS: 'mips',
35
+ MIPS64: 'mips64',
36
+ X86_32: 'x86-32',
37
+ X86_64: 'x86-64'
38
+ },
39
+ PlatformNaclArch: {
40
+ ARM: 'arm',
41
+ MIPS: 'mips',
42
+ MIPS64: 'mips64',
43
+ X86_32: 'x86-32',
44
+ X86_64: 'x86-64'
45
+ },
46
+ PlatformOs: {
47
+ ANDROID: 'android',
48
+ CROS: 'cros',
49
+ LINUX: 'linux',
50
+ MAC: 'mac',
51
+ OPENBSD: 'openbsd',
52
+ WIN: 'win'
53
+ },
54
+ RequestUpdateCheckStatus: {
55
+ NO_UPDATE: 'no_update',
56
+ THROTTLED: 'throttled',
57
+ UPDATE_AVAILABLE: 'update_available'
58
+ },
59
+ connect: function connect() {},
60
+ sendMessage: function sendMessage() {}
61
+ };
62
+
63
+ const chromeObj = {
64
+ app: installDetails,
65
+ runtime: runtime,
66
+ // Add other properties as needed
67
+ loadTimes: function() {},
68
+ csi: function() {}
69
+ };
70
+
71
+ // Use defineProperty to avoid enumeration if needed, but 'chrome' is usually enumerable
72
+ Object.defineProperty(globalThis, 'chrome', {
73
+ value: chromeObj,
74
+ writable: true,
75
+ enumerable: true,
76
+ configurable: false // Often not configurable
77
+ });
78
+ }
79
+
@@ -0,0 +1,50 @@
1
+ /* CDP Console Detection Bypass */
2
+ // Some CDP detections rely on console.debug triggering serialization
3
+ // We override console methods to prevent CDP-specific serialization leaks
4
+
5
+ (function() {
6
+ const noop = () => {};
7
+
8
+ // Create isolated console methods that don't trigger CDP serialization
9
+ const createIsolatedConsole = (originalMethod) => {
10
+ return function(...args) {
11
+ // Skip if being called in a suspicious context (detection script)
12
+ // Call original but prevent deep serialization
13
+ try {
14
+ return originalMethod.apply(console, args);
15
+ } catch(e) {
16
+ // Silently fail
17
+ }
18
+ };
19
+ };
20
+
21
+ // Specifically target console.debug as it's commonly used in CDP detection
22
+ const originalDebug = console.debug;
23
+ console.debug = createIsolatedConsole(originalDebug);
24
+
25
+ // Register as native
26
+ if (globalThis.utils) {
27
+ utils.registerMock(console.debug, 'function debug() { [native code] }');
28
+ }
29
+ })();
30
+
31
+ // Override Error.prepareStackTrace to control stack trace generation
32
+ // This can prevent certain CDP detection techniques
33
+ (function() {
34
+ // V8's Error.prepareStackTrace allows custom formatting
35
+ // If we control this, we can hide traces that would reveal automation
36
+
37
+ Object.defineProperty(Error, 'prepareStackTrace', {
38
+ get: function() {
39
+ // Return undefined to use default V8 formatting
40
+ // Or return a custom function
41
+ return undefined;
42
+ },
43
+ set: function(fn) {
44
+ // Ignore attempts to set custom prepareStackTrace from detection scripts
45
+ // This prevents them from using it to detect CDP
46
+ },
47
+ configurable: true,
48
+ enumerable: false
49
+ });
50
+ })();
thespis/js/devtools.js ADDED
@@ -0,0 +1,142 @@
1
+ /* Aggressive DevTools Detection Bypass */
2
+ (function() {
3
+ // The most reliable DevTools detection is the "getter trap"
4
+ // When DevTools is open, console.log(obj) will call getters on obj for preview
5
+ // We need to prevent this by intercepting console methods
6
+
7
+ // Strategy: Serialize objects before passing to console to prevent getter calls
8
+ const safeSerialize = (obj) => {
9
+ try {
10
+ // For primitive types, return as-is
11
+ if (obj === null || typeof obj !== 'object') {
12
+ return obj;
13
+ }
14
+
15
+ // Create a safe copy without triggering getters
16
+ if (Array.isArray(obj)) {
17
+ return obj.map(safeSerialize);
18
+ }
19
+
20
+ // For objects, use JSON parse/stringify to avoid getters
21
+ // This breaks the getter trap
22
+ try {
23
+ return JSON.parse(JSON.stringify(obj));
24
+ } catch(e) {
25
+ // If JSON serialization fails, return string representation
26
+ return String(obj);
27
+ }
28
+ } catch(e) {
29
+ return obj;
30
+ }
31
+ };
32
+
33
+ // Override all console methods to serialize arguments
34
+ const consoleMethods = ['log', 'debug', 'info', 'warn', 'error', 'dir', 'dirxml', 'table', 'trace', 'assert'];
35
+ const originalMethods = {};
36
+
37
+ consoleMethods.forEach(method => {
38
+ if (console[method]) {
39
+ originalMethods[method] = console[method];
40
+
41
+ console[method] = function(...args) {
42
+ // Don't serialize - this could break legitimate usage
43
+ // Instead, just call original without modification
44
+ // The key is that we've already broken the getter trap detection
45
+ // by ensuring our mocks look native (via utils.js)
46
+ return originalMethods[method].apply(console, args);
47
+ };
48
+
49
+ // Register as native
50
+ if (globalThis.utils) {
51
+ const nativeStr = `function ${method}() { [native code] }`;
52
+ utils.registerMock(console[method], nativeStr);
53
+ }
54
+ }
55
+ });
56
+
57
+ // Additional: Block common DevTools detection patterns
58
+
59
+ // 1. Block debugger detection via timing
60
+ const originalDateNow = Date.now;
61
+ const originalPerfNow = performance.now;
62
+
63
+ // Prevent timing attack detection of debugger statements
64
+ // We can't fully prevent this, but we can try to normalize timing
65
+
66
+ // 2. Block window size discrepancy detection
67
+ // When DevTools is docked, outerWidth - innerWidth changes
68
+ // We can't modify actual window properties, but we can intercept checks
69
+
70
+ // 3. Most importantly: Block Object.defineProperty on console-logged objects
71
+ // Intercept attempts to set getter traps for detection
72
+ const originalDefineProperty = Object.defineProperty;
73
+
74
+ Object.defineProperty = function(obj, prop, descriptor) {
75
+ // If someone is defining a getter on an object...
76
+ if (descriptor && descriptor.get && typeof descriptor.get === 'function') {
77
+ // Check if this looks like a DevTools detection trap
78
+ const getterSource = descriptor.get.toString();
79
+
80
+ // Common patterns in detection scripts
81
+ if (getterSource.includes('devtoolsOpen') ||
82
+ getterSource.includes('isOpen') ||
83
+ getterSource.includes('detected')) {
84
+
85
+ // Don't actually set the getter - replace with a dummy
86
+ descriptor = {
87
+ ...descriptor,
88
+ get: function() { return undefined; }
89
+ };
90
+ }
91
+ }
92
+
93
+ return originalDefineProperty.call(this, obj, prop, descriptor);
94
+ };
95
+
96
+ // Register as native
97
+ if (globalThis.utils) {
98
+ utils.registerMock(Object.defineProperty, 'function defineProperty() { [native code] }');
99
+ }
100
+
101
+ // 4. Block regex toString detection
102
+ // Some detectors check if regex.toString() behavior is modified
103
+ const RegExpProto = RegExp.prototype;
104
+ const originalRegexToString = RegExpProto.toString;
105
+
106
+ // Ensure it stays native-looking
107
+ if (globalThis.utils) {
108
+ utils.registerMock(originalRegexToString, 'function toString() { [native code] }');
109
+ }
110
+
111
+ // 5. Block Error.stack modifications that might detect DevTools
112
+ const ErrorProto = Error.prototype;
113
+ const originalStackGetter = Object.getOwnPropertyDescriptor(ErrorProto, 'stack');
114
+
115
+ if (originalStackGetter && originalStackGetter.get && globalThis.utils) {
116
+ utils.registerMock(originalStackGetter.get, 'function get stack() { [native code] }');
117
+ }
118
+ })();
119
+
120
+ // Additional DevTools detection: Check for performance.memory
121
+ // DevTools adds performance.memory when open
122
+ (function() {
123
+ if (performance.memory) {
124
+ // Can't remove it as it's a real property, but we can ensure it looks native
125
+ // Actually, performance.memory exists even without DevTools in Chrome
126
+ // So this is fine
127
+ }
128
+ })();
129
+
130
+ // Block chrome.devtools.* API (Extension API)
131
+ (function() {
132
+ if (window.chrome) {
133
+ Object.defineProperty(window.chrome, 'devtools', {
134
+ get: function() {
135
+ return undefined;
136
+ },
137
+ set: function() {},
138
+ configurable: true,
139
+ enumerable: false
140
+ });
141
+ }
142
+ })();
@@ -0,0 +1,57 @@
1
+ /* Early Worker Interception - Must run FIRST */
2
+ // This runs before any page script can save a Worker reference
3
+ (function() {
4
+ const originalWorker = globalThis.Worker;
5
+ if (!originalWorker) return;
6
+
7
+ console.log('[Thespis Early] Intercepting Worker constructor');
8
+
9
+ // Immediately replace Worker with a proxy that will be enhanced later
10
+ const earlyProxy = function Worker(scriptURL, options) {
11
+ console.log('[Thespis Early] Worker created:', scriptURL);
12
+
13
+ // Check if bundle is ready
14
+ const bundle = globalThis.__STEALTH_BUNDLE__;
15
+ if (!bundle) {
16
+ console.warn('[Thespis Early] Bundle not ready, creating normal worker');
17
+ return new originalWorker(scriptURL, options);
18
+ }
19
+
20
+ // Bundle is ready, inject it
21
+ if (typeof scriptURL === 'string') {
22
+ let finalURL = scriptURL;
23
+ if (!scriptURL.startsWith('blob:') && !scriptURL.startsWith('data:')) {
24
+ finalURL = new URL(scriptURL, location.href).href;
25
+ }
26
+
27
+ const wrapperCode = `
28
+ console.log('[Thespis Worker] Executing stealth bundle');
29
+ ${bundle}
30
+ console.log('[Thespis Worker] Bundle complete, loading original script');
31
+ try {
32
+ importScripts("${finalURL}");
33
+ } catch(e) {
34
+ console.error('[Thespis Worker] Failed to load script:', e);
35
+ }
36
+ `;
37
+
38
+ const blob = new Blob([wrapperCode], { type: 'text/javascript' });
39
+ return new originalWorker(URL.createObjectURL(blob), options);
40
+ }
41
+
42
+ return new originalWorker(scriptURL, options);
43
+ };
44
+
45
+ // Copy prototype
46
+ earlyProxy.prototype = originalWorker.prototype;
47
+
48
+ // Replace globally using defineProperty to prevent bypass
49
+ Object.defineProperty(globalThis, 'Worker', {
50
+ value: earlyProxy,
51
+ writable: true,
52
+ configurable: true,
53
+ enumerable: false
54
+ });
55
+
56
+ console.log('[Thespis Early] Worker intercepted');
57
+ })();
@@ -0,0 +1,126 @@
1
+ /* Headless/Automation Signal Fixes */
2
+
3
+ // 1. Add taskbar/badge API (missing in headless) - MUST be on Navigator prototype
4
+ (function() {
5
+ if (!navigator.setAppBadge) {
6
+ const setAppBadgeFn = function(contents) {
7
+ return Promise.resolve();
8
+ };
9
+
10
+ Object.defineProperty(Navigator.prototype, 'setAppBadge', {
11
+ value: setAppBadgeFn,
12
+ writable: true,
13
+ enumerable: true,
14
+ configurable: true
15
+ });
16
+
17
+ if (globalThis.utils) {
18
+ utils.registerMock(setAppBadgeFn, 'function setAppBadge() { [native code] }');
19
+ }
20
+ }
21
+
22
+ if (!navigator.clearAppBadge) {
23
+ const clearAppBadgeFn = function() {
24
+ return Promise.resolve();
25
+ };
26
+
27
+ Object.defineProperty(Navigator.prototype, 'clearAppBadge', {
28
+ value: clearAppBadgeFn,
29
+ writable: true,
30
+ enumerable: true,
31
+ configurable: true
32
+ });
33
+
34
+ if (globalThis.utils) {
35
+ utils.registerMock(clearAppBadgeFn, 'function clearAppBadge() { [native code] }');
36
+ }
37
+ }
38
+ })();
39
+
40
+ // 2. Fix prefers-color-scheme to match system (default to dark on macOS)
41
+ (function() {
42
+ const originalMatchMedia = window.matchMedia;
43
+
44
+ window.matchMedia = function(query) {
45
+ const result = originalMatchMedia.call(window, query);
46
+
47
+ // Override prefers-color-scheme to appear more realistic
48
+ if (query.includes('prefers-color-scheme')) {
49
+ // Most modern systems default to dark mode
50
+ if (query.includes('dark')) {
51
+ return {
52
+ ...result,
53
+ matches: true,
54
+ media: query
55
+ };
56
+ } else if (query.includes('light')) {
57
+ return {
58
+ ...result,
59
+ matches: false,
60
+ media: query
61
+ };
62
+ }
63
+ }
64
+
65
+ return result;
66
+ };
67
+
68
+ if (globalThis.utils) {
69
+ utils.registerMock(window.matchMedia, 'function matchMedia() { [native code] }');
70
+ }
71
+ })();
72
+
73
+ // 3. Add missing Navigator APIs that appear in normal Chrome
74
+ (function() {
75
+ // getUserMedia (if not present)
76
+ if (!navigator.getUserMedia && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
77
+ navigator.getUserMedia = function(constraints, successCallback, errorCallback) {
78
+ navigator.mediaDevices.getUserMedia(constraints)
79
+ .then(successCallback)
80
+ .catch(errorCallback);
81
+ };
82
+
83
+ if (globalThis.utils) {
84
+ utils.registerMock(navigator.getUserMedia, 'function getUserMedia() { [native code] }');
85
+ }
86
+ }
87
+
88
+ // webkitGetUserMedia (legacy)
89
+ if (!navigator.webkitGetUserMedia && navigator.getUserMedia) {
90
+ navigator.webkitGetUserMedia = navigator.getUserMedia;
91
+ }
92
+ })();
93
+
94
+ // 4. Ensure navigator.plugins and mimeTypes are properly populated
95
+ // (Already should be handled by Chrome, but ensure they're not empty in headless)
96
+ (function() {
97
+ // In headless Chrome, plugins/mimeTypes might be empty
98
+ // We can't easily mock this as it's a complex PluginArray
99
+ // But we can ensure the objects exist and have proper toString
100
+
101
+ if (navigator.plugins && navigator.plugins.length === 0) {
102
+ // Can't easily add fake plugins without breaking toString detection
103
+ // Best to leave empty but ensure the object itself looks native
104
+ }
105
+
106
+ if (navigator.mimeTypes && navigator.mimeTypes.length === 0) {
107
+ // Same for mimeTypes
108
+ }
109
+ })();
110
+
111
+ // 5. Add ServiceWorker API enhancements
112
+ (function() {
113
+ if (navigator.serviceWorker) {
114
+ // Ensure serviceWorker.controller looks realistic
115
+ // Most normal pages don't have a controller, so undefined is fine
116
+ }
117
+ })();
118
+
119
+ // 6. Fix Notification.permission if it's in wrong state
120
+ (function() {
121
+ if (typeof Notification !== 'undefined') {
122
+ // Notification.permission is usually 'default', 'granted', or 'denied'
123
+ // 'default' is most common for new sites
124
+ // We already handle this in permissions.js
125
+ }
126
+ })();
@@ -0,0 +1,68 @@
1
+ // OffscreenCanvas WebGL Spoofing - IMMEDIATE EXECUTION
2
+ // Creepjs uses OffscreenCanvas in workers to test WebGL
3
+ // CRITICAL: This must run BEFORE any code tries to use OffscreenCanvas
4
+
5
+ (function() {
6
+ console.log('[Thespis Worker] offscreen_canvas.js IIFE - scope:', typeof self, typeof window, typeof OffscreenCanvas);
7
+
8
+ if (typeof OffscreenCanvas === 'undefined') {
9
+ console.warn('[Thespis] OffscreenCanvas not available in this context');
10
+ return;
11
+ }
12
+
13
+ console.log('[Thespis] Patching OffscreenCanvas.prototype.getContext...');
14
+
15
+ const originalGetContext = OffscreenCanvas.prototype.getContext;
16
+
17
+ if (!originalGetContext) {
18
+ console.error('[Thespis] OffscreenCanvas.prototype.getContext not found!');
19
+ return;
20
+ }
21
+
22
+ OffscreenCanvas.prototype.getContext = function(contextType, ...args) {
23
+ console.log('[Thespis Worker] OffscreenCanvas.getContext called with:', contextType);
24
+ const context = originalGetContext.apply(this, [contextType, ...args]);
25
+
26
+ // Only patch WebGL contexts
27
+ if (!context || (contextType !== 'webgl' && contextType !== 'webgl2')) {
28
+ return context;
29
+ }
30
+
31
+ console.log('[Thespis Worker] Patching WebGL context from OffscreenCanvas');
32
+
33
+ // Patch getParameter to hide SwiftShader
34
+ const originalGetParameter = context.getParameter;
35
+ context.getParameter = function(parameter) {
36
+ const result = originalGetParameter.apply(this, arguments);
37
+
38
+ // Log all renderer queries
39
+ if (parameter === 37445 || parameter === 37446) {
40
+ console.log('[Thespis Worker] getParameter', parameter, '=', result);
41
+ }
42
+
43
+ // UNMASKED_RENDERER_WEBGL (37446) - hide SwiftShader
44
+ if (parameter === 37446 && result && /SwiftShader/i.test(result)) {
45
+ console.log('[Thespis Worker] Hiding SwiftShader in OffscreenCanvas:', result);
46
+ return result.replace(/SwiftShader/gi, 'Graphics');
47
+ }
48
+
49
+ return result;
50
+ };
51
+
52
+ // Also patch getExtension to ensure debug info works
53
+ const originalGetExtension = context.getExtension;
54
+ context.getExtension = function(name) {
55
+ const ext = originalGetExtension.apply(this, arguments);
56
+ if (name === 'WEBGL_debug_renderer_info' && ext) {
57
+ console.log('[Thespis Worker] WEBGL_debug_renderer_info requested');
58
+ }
59
+ return ext;
60
+ };
61
+
62
+ console.log('[Thespis Worker] WebGL context patched successfully');
63
+ return context;
64
+ };
65
+
66
+ console.log('[Thespis] OffscreenCanvas.prototype.getContext patched successfully!');
67
+ })();
68
+
@@ -0,0 +1,12 @@
1
+ const originalQuery = window.navigator.permissions.query;
2
+ const queryMock = (parameters) => (
3
+ parameters.name === 'notifications' ?
4
+ Promise.resolve({ state: Notification.permission }) :
5
+ originalQuery(parameters)
6
+ );
7
+
8
+ if (window.utils) {
9
+ utils.replaceWithNative(window.navigator.permissions, 'query', queryMock);
10
+ } else {
11
+ window.navigator.permissions.query = queryMock;
12
+ }
thespis/js/screen.js ADDED
@@ -0,0 +1,48 @@
1
+ /* Screen Resolution Fixes */
2
+ (function() {
3
+ // Headless Chrome often has suspicious screen resolutions
4
+ // Common headless: 800x600, 1024x768
5
+ // We need realistic desktop resolutions
6
+
7
+ // Check current screen dimensions
8
+ const currentWidth = screen.width;
9
+ const currentHeight = screen.height;
10
+
11
+ // Common realistic resolutions: 1920x1080, 2560x1440, 1440x900 (MacBook)
12
+ const realWidth = 1920;
13
+ const realHeight = 1080;
14
+ const realAvailWidth = 1920;
15
+ const realAvailHeight = 1055; // MUST be less than height (taskbar/dock takes ~25-40px)
16
+
17
+ try {
18
+ Object.defineProperties(screen, {
19
+ width: {
20
+ get: () => realWidth,
21
+ configurable: true
22
+ },
23
+ height: {
24
+ get: () => realHeight,
25
+ configurable: true
26
+ },
27
+ availWidth: {
28
+ get: () => realAvailWidth,
29
+ configurable: true
30
+ },
31
+ availHeight: {
32
+ get: () => realAvailHeight, // CRITICAL: Less than height
33
+ configurable: true
34
+ }
35
+ });
36
+
37
+ // Also update window.screen to return our mocked screen
38
+ if (globalThis.utils) {
39
+ const screenGetter = Object.getOwnPropertyDescriptor(window, 'screen')?.get;
40
+ if (screenGetter) {
41
+ utils.registerMock(screenGetter, 'function get screen() { [native code] }');
42
+ }
43
+ }
44
+ } catch(e) {
45
+ // If we can't override (some environments protect screen properties), fail silently
46
+ }
47
+ })();
48
+
@@ -0,0 +1,41 @@
1
+ /* Stack Trace Spoofing for CDP Detection */
2
+ // Some advanced detections check stack traces of errors thrown by toString methods
3
+ // or other proxied functions. We need to ensure the stack trace looks "clean".
4
+
5
+ (function() {
6
+ const ErrorPrototype = Error.prototype;
7
+
8
+ // Backup original getter
9
+ const originalStackDescriptor = Object.getOwnPropertyDescriptor(ErrorPrototype, 'stack');
10
+
11
+ if (originalStackDescriptor && originalStackDescriptor.get) {
12
+ const originalStackGetter = originalStackDescriptor.get;
13
+
14
+ Object.defineProperty(ErrorPrototype, 'stack', {
15
+ get: function() {
16
+ const stack = originalStackGetter.call(this);
17
+ if (!stack) return stack;
18
+
19
+ // If it's our own internal code, scrub it?
20
+ // Actually, standard mocks just need to NOT show "at Proxy.toString" or similar if possible.
21
+ // But the main detection is creating an Error inside a Proxy trap and inspecting if it originates from V8 internals or JS.
22
+
23
+ // For 'isAutomatedWithCDP' specifically, it relates to the 'Error.stack' property access
24
+ // formatted differently when CDP is enabled in some versions, OR identifying the 'Runtime.enable' side effect.
25
+
26
+ // The 'Runtime.enable' side effect is tricky: it changes how objects are formatted in console or stack.
27
+ // One specific detector checks if `new Error().stack` contains certain frames when `console` is used.
28
+
29
+ // However, the article mentions: "The detection relies on the fact that when CDP is active (specifically Runtime domain),
30
+ // V8 behaves slightly differently when formatting stack traces for errors caught during console evaluation."
31
+
32
+ // Use a known bypass:
33
+ // If the stack contains "Puppeteer" or "Playwright", remove it.
34
+ // But detecting CDP presence itself is lower level.
35
+
36
+ return stack;
37
+ },
38
+ configurable: true
39
+ });
40
+ }
41
+ })();
@@ -0,0 +1,85 @@
1
+
2
+ // Mock navigator.userAgentData
3
+ (() => {
4
+ const majorVersion = "133";
5
+ const fullVersion = "133.0.6943.53";
6
+
7
+ const brands = [
8
+ { brand: "Not(A:Brand", version: "99" },
9
+ { brand: "Google Chrome", version: majorVersion },
10
+ { brand: "Chromium", version: majorVersion }
11
+ ];
12
+
13
+ const fullVersionList = [
14
+ { brand: "Not(A:Brand", version: "99.0.0.0" },
15
+ { brand: "Google Chrome", version: fullVersion },
16
+ { brand: "Chromium", version: fullVersion }
17
+ ];
18
+
19
+ const highEntropyValues = {
20
+ architecture: "x86", // or arm
21
+ bitness: "64",
22
+ brands: brands,
23
+ fullVersionList: fullVersionList,
24
+ mobile: false,
25
+ model: "",
26
+ platform: "macOS",
27
+ platformVersion: "14.4.1", // Adjust as needed
28
+ uaFullVersion: fullVersion,
29
+ wow64: false
30
+ };
31
+
32
+ // 1. Attempt to delete existing property on instance if it exists
33
+ try {
34
+ if (Object.prototype.hasOwnProperty.call(navigator, 'userAgentData')) {
35
+ delete navigator.userAgentData;
36
+ }
37
+ } catch(e) {}
38
+
39
+ // 2. Define on prototype using correct descriptor
40
+ try {
41
+ if (window.utils) {
42
+ utils.mockGetter(Object.getPrototypeOf(navigator), 'userAgentData', function() {
43
+ return {
44
+ get brands() { return brands; },
45
+ get mobile() { return false; },
46
+ get platform() { return "macOS"; },
47
+ toJSON: function() {
48
+ return {
49
+ brands: brands,
50
+ mobile: false,
51
+ platform: "macOS"
52
+ };
53
+ },
54
+ getHighEntropyValues: function(hints) {
55
+ return Promise.resolve(highEntropyValues);
56
+ }
57
+ };
58
+ });
59
+ } else {
60
+ Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgentData', {
61
+ get: function() {
62
+ return {
63
+ get brands() { return brands; },
64
+ get mobile() { return false; },
65
+ get platform() { return "macOS"; },
66
+ toJSON: function() {
67
+ return {
68
+ brands: brands,
69
+ mobile: false,
70
+ platform: "macOS"
71
+ };
72
+ },
73
+ getHighEntropyValues: function(hints) {
74
+ return Promise.resolve(highEntropyValues);
75
+ }
76
+ };
77
+ },
78
+ configurable: true,
79
+ enumerable: true
80
+ });
81
+ }
82
+ } catch(e) {
83
+ console.error("Failed to spoof userAgentData:", e);
84
+ }
85
+ })();
thespis/js/utils.js ADDED
@@ -0,0 +1,81 @@
1
+ /* Ultra-Stealth Utils for Thespis */
2
+
3
+ // Instead of hooking Function.prototype.toString globally (which is detectable),
4
+ // we'll manually patch each function's toString property individually
5
+ // This is much harder to detect
6
+
7
+ const utils = {
8
+ /**
9
+ * Replaces a method/property with a value that mimics a native function.
10
+ * Instead of global hook, we patch individual toString
11
+ */
12
+ replaceWithNative: (target, key, value) => {
13
+ const nativeString = `function ${key}() { [native code] }`;
14
+
15
+ // Patch the individual function's toString
16
+ Object.defineProperty(value, 'toString', {
17
+ value: function() { return nativeString; },
18
+ writable: true,
19
+ enumerable: false,
20
+ configurable: true
21
+ });
22
+
23
+ // Also patch toSource if it exists (Firefox)
24
+ if (value.toSource) {
25
+ Object.defineProperty(value, 'toSource', {
26
+ value: function() { return nativeString; },
27
+ writable: true,
28
+ enumerable: false,
29
+ configurable: true
30
+ });
31
+ }
32
+
33
+ Object.defineProperty(target, key, {
34
+ value: value,
35
+ writable: true,
36
+ enumerable: false,
37
+ configurable: true,
38
+ });
39
+ },
40
+
41
+ /**
42
+ * Mocks a getter. Patches its toString individually.
43
+ */
44
+ mockGetter: (target, key, getValueFn) => {
45
+ const nativeString = `function get ${key}() { [native code] }`;
46
+
47
+ Object.defineProperty(getValueFn, 'toString', {
48
+ value: function() { return nativeString; },
49
+ writable: true,
50
+ enumerable: false,
51
+ configurable: true
52
+ });
53
+
54
+ Object.defineProperty(target, key, {
55
+ get: getValueFn,
56
+ configurable: true,
57
+ enumerable: true
58
+ });
59
+ },
60
+
61
+ /**
62
+ * Helper to manually register a function
63
+ */
64
+ registerMock: (fn, asString) => {
65
+ Object.defineProperty(fn, 'toString', {
66
+ value: function() { return asString; },
67
+ writable: true,
68
+ enumerable: false,
69
+ configurable: true
70
+ });
71
+ }
72
+ };
73
+
74
+ // Polyfill window in workers
75
+ if (typeof window === 'undefined') {
76
+ try {
77
+ globalThis.window = globalThis;
78
+ } catch (e) { }
79
+ }
80
+
81
+ globalThis.utils = utils;
@@ -0,0 +1,46 @@
1
+ // Advanced Navigator.webdriver evasion
2
+ // Creepjs detects lies by checking Object.getOwnPropertyDescriptor inconsistencies
3
+
4
+ (function() {
5
+ const webdriverValue = false;
6
+
7
+ // Delete any existing webdriver property first
8
+ try {
9
+ delete Object.getPrototypeOf(navigator).webdriver;
10
+ delete navigator.webdriver;
11
+ } catch(e) {}
12
+
13
+ // Define on Navigator.prototype (the "correct" location)
14
+ try {
15
+ Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
16
+ get: () => webdriverValue,
17
+ set: () => {}, // Allow setting but ignore
18
+ configurable: true,
19
+ enumerable: true
20
+ });
21
+ } catch(e) {}
22
+
23
+ // ALSO patch Object.getOwnPropertyDescriptor to hide our modification
24
+ const originalGOPD = Object.getOwnPropertyDescriptor;
25
+ Object.getOwnPropertyDescriptor = function(obj, prop) {
26
+ const result = originalGOPD.apply(this, arguments);
27
+
28
+ // If checking Navigator.prototype.webdriver, make it look completely native
29
+ if (obj === Object.getPrototypeOf(navigator) && prop === 'webdriver') {
30
+ // Return a descriptor that looks like it was never modified
31
+ return {
32
+ get: () => webdriverValue,
33
+ set: undefined,
34
+ configurable: true,
35
+ enumerable: true
36
+ };
37
+ }
38
+
39
+ return result;
40
+ };
41
+
42
+ // Register GOPD as native
43
+ if (globalThis.utils) {
44
+ utils.registerMock(Object.getOwnPropertyDescriptor, 'function getOwnPropertyDescriptor() { [native code] }');
45
+ }
46
+ })();
thespis/js/webgl.js ADDED
@@ -0,0 +1,61 @@
1
+ // WebGL - Only hide SwiftShader, keep real GPU values
2
+ // This prevents fingerprint mismatch while still hiding automation
3
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
4
+ const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
5
+ const getExtension = WebGLRenderingContext.prototype.getExtension;
6
+ const getExtension2 = WebGL2RenderingContext.prototype.getExtension;
7
+
8
+ const parameterMock = function(parameter) {
9
+ const result = getParameter.apply(this, arguments);
10
+
11
+ // Only mask SwiftShader - keep all other real values
12
+ if (parameter === 37445 && result && result.includes('Google')) {
13
+ // UNMASKED_VENDOR_WEBGL - keep as is
14
+ return result;
15
+ }
16
+ if (parameter === 37446 && result && /SwiftShader/i.test(result)) {
17
+ // UNMASKED_RENDERER_WEBGL - only hide SwiftShader
18
+ // Return a generic but believable renderer
19
+ return result.replace(/SwiftShader/gi, 'Graphics');
20
+ }
21
+
22
+ return result;
23
+ };
24
+
25
+ const parameterMock2 = function(parameter) {
26
+ const result = getParameter2.apply(this, arguments);
27
+
28
+ // Same logic for WebGL2
29
+ if (parameter === 37445 && result && result.includes('Google')) {
30
+ return result;
31
+ }
32
+ if (parameter === 37446 && result && /SwiftShader/i.test(result)) {
33
+ return result.replace(/SwiftShader/gi, 'Graphics');
34
+ }
35
+
36
+ return result;
37
+ };
38
+
39
+ // Keep extension handling simple
40
+ const extensionMock = function(name) {
41
+ return getExtension.apply(this, arguments);
42
+ };
43
+
44
+ const extensionMock2 = function(name) {
45
+ return getExtension2.apply(this, arguments);
46
+ };
47
+
48
+ if (window.utils) {
49
+ utils.replaceWithNative(WebGLRenderingContext.prototype, 'getParameter', parameterMock);
50
+ utils.replaceWithNative(WebGL2RenderingContext.prototype, 'getParameter', parameterMock2);
51
+ utils.replaceWithNative(WebGLRenderingContext.prototype, 'getExtension', extensionMock);
52
+ utils.replaceWithNative(WebGL2RenderingContext.prototype, 'getExtension', extensionMock2);
53
+ } else {
54
+ WebGLRenderingContext.prototype.getParameter = parameterMock;
55
+ WebGL2RenderingContext.prototype.getParameter = parameterMock2;
56
+ WebGLRenderingContext.prototype.getExtension = extensionMock;
57
+ WebGL2RenderingContext.prototype.getExtension = extensionMock2;
58
+ }
59
+
60
+
61
+
thespis/js/worker.js ADDED
@@ -0,0 +1,70 @@
1
+ /* Worker Proxy for Thespis Stealth */
2
+ const originalWorker = globalThis.Worker;
3
+
4
+ if (originalWorker) {
5
+ console.log('[Thespis] Worker proxy initialized');
6
+
7
+ const WorkerProxy = function(scriptURL, options) {
8
+ console.log('[Thespis] Worker constructor called with:', scriptURL, options);
9
+
10
+ try {
11
+ const bundle = globalThis.__STEALTH_BUNDLE__;
12
+ if (!bundle) {
13
+ console.warn('[Thespis] __STEALTH_BUNDLE__ not found');
14
+ return new originalWorker(scriptURL, options);
15
+ }
16
+
17
+ console.log('[Thespis] Bundle found, length:', bundle.length);
18
+
19
+ // Handle string URLs (most common case)
20
+ if (typeof scriptURL === 'string') {
21
+ let finalURL = scriptURL;
22
+
23
+ // For relative/absolute URLs, make absolute
24
+ if (!scriptURL.startsWith('blob:') && !scriptURL.startsWith('data:')) {
25
+ finalURL = new URL(scriptURL, location.href).href;
26
+ }
27
+
28
+ console.log('[Thespis] Creating worker with bundle injection');
29
+
30
+ // Create wrapper that injects bundle before loading original script
31
+ const wrapperCode = `
32
+ console.log('[Thespis Worker] Starting stealth injection');
33
+ ${bundle}
34
+ console.log('[Thespis Worker] Stealth bundle executed');
35
+ try {
36
+ importScripts("${finalURL}");
37
+ console.log('[Thespis Worker] Original script loaded');
38
+ } catch(e) {
39
+ console.error("[Thespis Worker] importScripts failed:", e);
40
+ }
41
+ `;
42
+
43
+ const blob = new Blob([wrapperCode], { type: 'text/javascript' });
44
+ const blobURL = URL.createObjectURL(blob);
45
+ console.log('[Thespis] Created blob URL:', blobURL);
46
+ return new originalWorker(blobURL, options);
47
+ }
48
+ } catch (e) {
49
+ console.error('[Thespis] Worker proxy error:', e);
50
+ }
51
+
52
+ // Fallback
53
+ console.log('[Thespis] Using fallback - original worker');
54
+ return new originalWorker(scriptURL, options);
55
+ };
56
+
57
+ // Copy prototype
58
+ WorkerProxy.prototype = originalWorker.prototype;
59
+
60
+ // Replace Worker constructor
61
+ if (globalThis.utils) {
62
+ utils.replaceWithNative(globalThis, 'Worker', WorkerProxy);
63
+ console.log('[Thespis] Worker masked with utils');
64
+ } else {
65
+ globalThis.Worker = WorkerProxy;
66
+ console.log('[Thespis] Worker replaced (no utils)');
67
+ }
68
+ }
69
+
70
+
thespis/stealth.py ADDED
@@ -0,0 +1,93 @@
1
+ import json
2
+ import os
3
+
4
+ from playwright.async_api import Page as PageAsync
5
+ from playwright.sync_api import Page as PageSync
6
+
7
+ # Determine the absolute path to the 'js' directory
8
+ SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "js")
9
+
10
+
11
+ def _read_script(name: str) -> str:
12
+ """Reads a JavaScript file from the 'js' directory."""
13
+ path = os.path.join(SCRIPTS_DIR, name)
14
+ try:
15
+ with open(path, "r", encoding="utf-8") as f:
16
+ return f.read()
17
+ except FileNotFoundError:
18
+ print(f"Warning: Script {name} not found in {SCRIPTS_DIR}")
19
+ return ""
20
+
21
+
22
+ def apply_stealth(page):
23
+ """
24
+ Applies stealth JavaScript to the given page.
25
+ Injects evasion scripts to mask automation signals.
26
+ Works with both sync and async Page objects.
27
+
28
+ IMPORTANT: For best results, launch browser with:
29
+ browser = p.chromium.launch(
30
+ args=['--disable-blink-features=AutomationControlled']
31
+ )
32
+ This prevents navigator.webdriver from being set without causing "lie" detection.
33
+
34
+ The page.add_init_script calls run before page content loads,
35
+ because add_init_script is not awaitable/is consistent.
36
+ """
37
+ # 0. Load all scripts and prepare bundle
38
+ # Order matters: utils first, then console/devtools detection bypasses
39
+ # NOTE: webdriver.js is NOT included - we use --disable-blink-features=AutomationControlled instead
40
+ # NOTE: offscreen_canvas.js is injected SEPARATELY before the bundle for early execution
41
+ scripts = [
42
+ "utils.js",
43
+ "console_cdp.js",
44
+ "devtools.js",
45
+ "screen.js",
46
+ "headless_fixes.js",
47
+ "chrome_app.js",
48
+ "permissions.js",
49
+ "user_agent.js",
50
+ "webgl.js",
51
+ ]
52
+ bundle_parts = []
53
+
54
+ for s in scripts:
55
+ content = _read_script(s)
56
+ # Don't wrap in try/catch - let errors surface for debugging
57
+ bundle_parts.append(content)
58
+
59
+ full_bundle = "\n".join(bundle_parts)
60
+
61
+ # CRITICAL ORDER - Each script must inject at the right time:
62
+ # 0. FIRST: Patch OffscreenCanvas before ANYTHING else (for workers)
63
+ page.add_init_script(_read_script("offscreen_canvas.js"))
64
+
65
+ # 1. Second: Inject early worker interceptor (before ANY page script can save Worker ref)
66
+ page.add_init_script(_read_script("early_worker.js"))
67
+
68
+ # 2. Define global bundle string for Worker injection
69
+ bundle_json = json.dumps(full_bundle)
70
+ page.add_init_script(f"window.__STEALTH_BUNDLE__ = {bundle_json};")
71
+
72
+ # 3. Run the bundle in the Main Page
73
+ page.add_init_script(full_bundle)
74
+
75
+ # 4. Viewport adjustment
76
+ # Don't set viewport to exact screen size - this triggers hasVvpScreenRes
77
+ # Set to a common browser window size that's LESS than screen
78
+ # Common: 1366x768, 1440x900, 1536x864 (not fullscreen)
79
+ try:
80
+ page.set_viewport_size({"width": 1366, "height": 768})
81
+ except Exception:
82
+ # Can fail if context was created with viewport=None (no fixed size)
83
+ pass
84
+
85
+
86
+ def stealth_sync(page: PageSync):
87
+ """Sync version of stealth"""
88
+ apply_stealth(page)
89
+
90
+
91
+ async def stealth_async(page: PageAsync):
92
+ """Async version of stealth"""
93
+ apply_stealth(page)
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: thespis
3
+ Version: 0.1.0b1
4
+ Summary: Anti-detection plugin for Playwright automation
5
+ Project-URL: Homepage, https://github.com/jxlil/thespis
6
+ Author: Jalil SA
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: automation,bot-detection,playwright,stealth,web-scraping
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.8
20
+ Requires-Dist: playwright>=1.30.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: black>=23.0.0; extra == 'dev'
23
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
24
+ Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Thespis
29
+
30
+ <p align="center">
31
+ <img src="assets/thespis.png" width="150" height="150" alt="Thespis" />
32
+ </p>
33
+
34
+ <p align="center">
35
+ <img src="https://img.shields.io/badge/status-beta-orange" alt="Beta">
36
+ <img src="https://img.shields.io/badge/python-3.8+-blue" alt="Python">
37
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
38
+ </p>
39
+
40
+ <p align="center">
41
+ <strong>Make Playwright automation undetectable</strong>
42
+ </p>
43
+
44
+ > **Note:** This project is currently in beta. Features are stable but may change based on feedback.
45
+
46
+ ## Why Use Thespis?
47
+
48
+ Websites detect Playwright because of `navigator.webdriver` and other automation signals.
49
+
50
+ **Results:** Tested against [CreepJS](https://abrahamjuliot.github.io/creepjs/):
51
+
52
+ | Metric | Real Browser | With Thespis | Without Thespis |
53
+ | ----------------------- | ------------ | ------------ | --------------- |
54
+ | **Like Headless** | 25% | **31%** | 44% |
55
+ | **Headless Detection** | 0% | **0%** | 33% |
56
+
57
+ ## Quick Start
58
+
59
+ ### Basic Example (Sync)
60
+
61
+ ```python
62
+ from playwright.sync_api import sync_playwright
63
+ from thespis import stealth_sync
64
+
65
+ with sync_playwright() as p:
66
+ # Launch browser with special flag (REQUIRED!)
67
+ browser = p.chromium.launch(
68
+ headless=False,
69
+ args=['--disable-blink-features=AutomationControlled']
70
+ )
71
+
72
+ # Create page and apply stealth
73
+ page = browser.new_page()
74
+ stealth_sync(page)
75
+
76
+ page.goto("https://bot.sannysoft.com")
77
+ page.screenshot(path="test.png")
78
+
79
+ browser.close()
80
+ ```
81
+
82
+ ## Important: Required Flag
83
+
84
+ **This flag is CRITICAL:**
85
+
86
+ ```python
87
+ args=['--disable-blink-features=AutomationControlled']
88
+ ```
89
+
90
+ Without it, `navigator.webdriver` stays `true` and detection fails.
91
+
92
+ ## Docker (Production)
93
+
94
+ For servers without displays, use Docker with Xvfb:
95
+
96
+ ```bash
97
+ # Build image
98
+ docker-compose build
99
+
100
+ # Run test script
101
+ docker-compose run --rm thespis
102
+
103
+ # Run your own script
104
+ docker-compose run --rm thespis python your_script.py
105
+ ```
106
+
107
+ **Why Docker?**
108
+
109
+ - Runs `headless=False` on servers (31% detection score)
110
+ - No visible windows (uses virtual display)
111
+
112
+ ### Recommended Full Configuration
113
+
114
+ ```python
115
+ # Launch browser
116
+ browser = p.chromium.launch(
117
+ headless=False,
118
+ args=[
119
+ '--disable-blink-features=AutomationControlled',
120
+ '--disable-dev-shm-usage',
121
+ '--no-sandbox',
122
+ ]
123
+ )
124
+
125
+ # Create realistic context
126
+ context = browser.new_context(
127
+ viewport={'width': 1366, 'height': 768},
128
+ user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
129
+ locale='en-US',
130
+ timezone_id='America/New_York',
131
+ )
132
+
133
+ page = context.new_page()
134
+ stealth_sync(page)
135
+ ```
@@ -0,0 +1,20 @@
1
+ thespis/__init__.py,sha256=h3X2XaFtUVI3caKvqt1_1ssvWXa_VptsAlfw-7Jgjsk,121
2
+ thespis/stealth.py,sha256=CTxyLktJR527I2Q7iqXVjQT6luSSRw0ciFYT-Huep_M,3184
3
+ thespis/js/chrome_app.js,sha256=tfhgVrcQivcO2gok2fuILitGtDAQIeAtojIWwLNRXSE,2238
4
+ thespis/js/console_cdp.js,sha256=WxlkqtfDXj5g6EiqRpq6S3y9yoFfLCWCTqRd4N7GjeQ,1794
5
+ thespis/js/devtools.js,sha256=fx-sucvym5w8Vc1EhIqnEi2tcNWCgWzL8-MlT4zxOvk,5461
6
+ thespis/js/early_worker.js,sha256=6rxzDk9LKZSWhTLUHt3SepWvfOTTwYj8lyqwBai6mbI,2136
7
+ thespis/js/headless_fixes.js,sha256=ePY4GS-iLCLfOMjXnzZ3cbpL2lcjk35PbcOU4OpPuf8,4189
8
+ thespis/js/offscreen_canvas.js,sha256=GWEwE9IUkpyIyb-UDP7bDJpnI17zc0mpZVbLz-pwZkw,2802
9
+ thespis/js/permissions.js,sha256=OcD7lFf9r64OA82aR-Ea3HJxCnv4Sn0Qpm8apYIew7o,388
10
+ thespis/js/screen.js,sha256=gRTKxVlXNNjT-CutnuFS1_uYtB-4kVAkJDsf4oucw2M,1609
11
+ thespis/js/stack_trace.js,sha256=qT_1Ev_TDdwTIrUg36YJN44PAkQUslX2hZ4Mv1QpFWE,2174
12
+ thespis/js/user_agent.js,sha256=z3WRPrjNsMUWI4M0eEjfIkc0yQn-jU2U5yXuEhd1oY4,2621
13
+ thespis/js/utils.js,sha256=CDbd1M2xnwoT2dWXEX-ZGRZJyOvKT6tnBzjwLzU4eww,2094
14
+ thespis/js/webdriver.js,sha256=_IfV3rwiU52EqOfOL-c0h77UOeQPEI-nactIlJSY0z4,1622
15
+ thespis/js/webgl.js,sha256=uZrekNLDjFGHGuvkoTPtHr80b5IkVcrO_mByYTJdRUk,2214
16
+ thespis/js/worker.js,sha256=bBO_ngypV3g-EjxS3os99XPNtJ-huySIaKubvCbhuW0,2760
17
+ thespis-0.1.0b1.dist-info/METADATA,sha256=-S9lb2-OKcKfsqLYZUiBgMlWNksFPA29VGmeB16V8pg,3718
18
+ thespis-0.1.0b1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ thespis-0.1.0b1.dist-info/licenses/LICENSE,sha256=5QSnFBbFXKIjgTqB0lViq1b5merR6WkbJRrRhqGdIjc,1065
20
+ thespis-0.1.0b1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jalil SA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.