@mywallpaper/addon-sdk 2.6.0 → 2.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mywallpaper/addon-sdk",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "SDK for building MyWallpaper addons - TypeScript types, manifest validation, and utilities",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
+ "src/runtime",
22
23
  "README.md",
23
24
  "LICENSE"
24
25
  ],
@@ -64,4 +65,4 @@
64
65
  "engines": {
65
66
  "node": ">=18.0.0"
66
67
  }
67
- }
68
+ }
@@ -0,0 +1,489 @@
1
+ /* eslint-disable no-var, curly, object-shorthand */
2
+ /**
3
+ * MyWallpaper Addon SDK v2.0
4
+ *
5
+ * API disponible pour les addons:
6
+ * - window.MyWallpaper.config → Configuration actuelle
7
+ * - window.MyWallpaper.layerId → ID du layer
8
+ * - window.MyWallpaper.onSettingsChange(fn) → Callback hot-reload
9
+ * - window.MyWallpaper.onEvent(type, fn) → Événements système
10
+ * - window.MyWallpaper.ready(options) → Signal prêt
11
+ * - window.MyWallpaper.renderComplete() → Signal rendu visuel terminé
12
+ *
13
+ * Note: Uses 'var' intentionally for maximum iframe/browser compatibility
14
+ */
15
+ ; (function () {
16
+ 'use strict'
17
+
18
+ var layerId = window.__MYWALLPAPER_LAYER_ID__ || ''
19
+ var config = window.__MYWALLPAPER_INITIAL_CONFIG__ || {}
20
+ var isReady = false
21
+ var isRenderComplete = false
22
+
23
+ var settingsCallbacks = []
24
+ var eventCallbacks = new Map()
25
+ var lifecycleCallbacks = { mount: [], unmount: [], pause: [], resume: [] }
26
+ var pendingStorage = new Map()
27
+ var pendingPermissions = new Map()
28
+ var pendingNetwork = new Map()
29
+ var pendingOAuth = new Map()
30
+ var pendingOAuthScopes = new Map()
31
+ var pendingFileAccess = new Map()
32
+ var grantedBlobUrls = new Map() // settingKey -> blobUrl
33
+
34
+ // Get the original fetch saved by network-sandbox.js (before it was blocked)
35
+ var originalFetch = window.__MYWALLPAPER_ORIGINAL_FETCH__ || null
36
+
37
+ // Message handler
38
+ function onMessage(e) {
39
+ if (e.source !== window.parent) return
40
+ var d = e.data
41
+ if (!d || d.source !== 'MyWallpaperHost') return
42
+
43
+ switch (d.type) {
44
+ case 'SETTINGS_UPDATE':
45
+ var p = d.payload || d
46
+ config = p.settings || p
47
+ var keys = p.changedKeys || Object.keys(config)
48
+ settingsCallbacks.forEach(function (cb) {
49
+ try { cb(config, keys) } catch (err) { console.error('[MyWallpaper]', err) }
50
+ })
51
+ break
52
+
53
+ case 'SYSTEM_EVENT':
54
+ var cbs = eventCallbacks.get(d.payload.event)
55
+ if (cbs) cbs.forEach(function (cb) {
56
+ try { cb(d.payload.data) } catch (err) { console.error('[MyWallpaper]', err) }
57
+ })
58
+ break
59
+
60
+ case 'LIFECYCLE':
61
+ var lcbs = lifecycleCallbacks[d.payload.event]
62
+ if (lcbs) lcbs.forEach(function (cb) {
63
+ try { cb() } catch (err) { console.error('[MyWallpaper]', err) }
64
+ })
65
+ break
66
+
67
+ case 'STORAGE_RESPONSE':
68
+ var op = pendingStorage.get(d.payload.requestId)
69
+ if (op) {
70
+ pendingStorage.delete(d.payload.requestId)
71
+ d.payload.success ? op.resolve(d.payload.value) : op.reject(new Error(d.payload.error))
72
+ }
73
+ break
74
+
75
+ case 'PERMISSION_GRANT':
76
+ var permOp = pendingPermissions.get(d.payload.requestId)
77
+ if (permOp) {
78
+ pendingPermissions.delete(d.payload.requestId)
79
+ permOp(d.payload.granted === true)
80
+ }
81
+ break
82
+
83
+ case 'NETWORK_RESPONSE':
84
+ var netOp = pendingNetwork.get(d.payload.requestId)
85
+ if (netOp) {
86
+ pendingNetwork.delete(d.payload.requestId)
87
+ if (d.payload.success) {
88
+ netOp.resolve({
89
+ ok: d.payload.status >= 200 && d.payload.status < 300,
90
+ status: d.payload.status || 200,
91
+ statusText: d.payload.statusText || 'OK',
92
+ headers: d.payload.headers || {},
93
+ data: d.payload.data
94
+ })
95
+ } else {
96
+ netOp.reject(new Error(d.payload.error || 'Network request failed'))
97
+ }
98
+ }
99
+ break
100
+
101
+ case 'OAUTH_RESPONSE':
102
+ var oauthOp = pendingOAuth.get(d.payload.requestId)
103
+ if (oauthOp) {
104
+ pendingOAuth.delete(d.payload.requestId)
105
+ if (d.payload.success) {
106
+ // Check if it's a scopes error
107
+ if (d.payload.error === 'insufficient_scopes') {
108
+ oauthOp.resolve({
109
+ error: 'insufficient_scopes',
110
+ message: d.payload.message,
111
+ provider: d.payload.provider,
112
+ missingScopes: d.payload.missing_scopes || d.payload.missingScopes || [],
113
+ requiredScopes: d.payload.required_scopes || d.payload.requiredScopes || []
114
+ })
115
+ } else {
116
+ oauthOp.resolve({
117
+ ok: d.payload.status >= 200 && d.payload.status < 300,
118
+ status: d.payload.status || 200,
119
+ headers: d.payload.headers || {},
120
+ data: d.payload.data
121
+ })
122
+ }
123
+ } else {
124
+ // Check if it's a scopes error from failed request
125
+ if (d.payload.error === 'insufficient_scopes' ||
126
+ (d.payload.data && d.payload.data.error === 'insufficient_scopes')) {
127
+ var scopesData = d.payload.data || d.payload
128
+ oauthOp.resolve({
129
+ error: 'insufficient_scopes',
130
+ message: scopesData.message || 'Insufficient OAuth scopes',
131
+ provider: scopesData.provider,
132
+ missingScopes: scopesData.missing_scopes || scopesData.missingScopes || [],
133
+ requiredScopes: scopesData.required_scopes || scopesData.requiredScopes || []
134
+ })
135
+ } else {
136
+ oauthOp.reject(new Error(d.payload.error || 'OAuth request failed'))
137
+ }
138
+ }
139
+ }
140
+ break
141
+
142
+ case 'OAUTH_SCOPES_GRANT':
143
+ var scopeResolver = pendingOAuthScopes.get(d.payload.requestId)
144
+ if (scopeResolver) {
145
+ pendingOAuthScopes.delete(d.payload.requestId)
146
+ scopeResolver(d.payload.granted === true)
147
+ }
148
+ break
149
+
150
+ case 'FILE_ACCESS_RESPONSE':
151
+ // Handle file access response from host
152
+ // Host sends us a Blob object (cloned via structured clone)
153
+ // We create our own blob URL from it
154
+ var fileOp = pendingFileAccess.get(d.requestId)
155
+ if (fileOp) {
156
+ pendingFileAccess.delete(d.requestId)
157
+ if (d.granted && d.blob instanceof Blob) {
158
+ // Create our own blob URL from the received Blob
159
+ var blobUrl = URL.createObjectURL(d.blob)
160
+ grantedBlobUrls.set(d.settingKey, blobUrl)
161
+ fileOp.resolve({ granted: true, blobUrl: blobUrl })
162
+ } else {
163
+ fileOp.resolve({ granted: false, error: d.error || 'Access denied' })
164
+ }
165
+ }
166
+ break
167
+ }
168
+ }
169
+
170
+ function send(type, payload) {
171
+ window.parent.postMessage({ type: type, layerId: layerId, timestamp: Date.now(), payload: payload }, '*')
172
+ }
173
+
174
+ function storageOp(op, key, value) {
175
+ return new Promise(function (resolve, reject) {
176
+ var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
177
+ pendingStorage.set(id, { resolve: resolve, reject: reject })
178
+ setTimeout(function () {
179
+ if (pendingStorage.has(id)) {
180
+ pendingStorage.delete(id)
181
+ reject(new Error('Storage timeout'))
182
+ }
183
+ }, 5000)
184
+ send('STORAGE_OPERATION', { operation: op, key: key, value: value, requestId: id })
185
+ })
186
+ }
187
+
188
+ /**
189
+ * Network fetch via secure host proxy
190
+ * This is the ONLY way addons can make network requests
191
+ * Domain must be declared in manifest.json permissions.network.domains
192
+ */
193
+ function networkFetch(url, options) {
194
+ return new Promise(function (resolve, reject) {
195
+ var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
196
+ pendingNetwork.set(id, { resolve: resolve, reject: reject })
197
+ // 30s timeout for network requests
198
+ setTimeout(function () {
199
+ if (pendingNetwork.has(id)) {
200
+ pendingNetwork.delete(id)
201
+ reject(new Error('Network request timeout (30s)'))
202
+ }
203
+ }, 30000)
204
+ options = options || {}
205
+ send('NETWORK_REQUEST', {
206
+ url: url,
207
+ method: options.method || 'GET',
208
+ headers: options.headers,
209
+ body: options.body,
210
+ requestId: id
211
+ })
212
+ })
213
+ }
214
+
215
+ /**
216
+ * OAuth API request via secure host proxy
217
+ * Makes authenticated API calls to OAuth providers (GitHub, Google, etc.)
218
+ * Tokens are stored securely on the server - never exposed to addons
219
+ *
220
+ * @param {string} provider - OAuth provider ('github', 'google', 'discord', 'spotify', 'twitch')
221
+ * @param {string} endpoint - API endpoint path (e.g., '/user', '/user/repos')
222
+ * @param {object} options - Request options (method, body, headers, requiredScopes)
223
+ */
224
+ function oauthRequest(provider, endpoint, options) {
225
+ return new Promise(function (resolve, reject) {
226
+ var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
227
+ pendingOAuth.set(id, { resolve: resolve, reject: reject })
228
+ // 30s timeout for OAuth requests
229
+ setTimeout(function () {
230
+ if (pendingOAuth.has(id)) {
231
+ pendingOAuth.delete(id)
232
+ reject(new Error('OAuth request timeout (30s)'))
233
+ }
234
+ }, 30000)
235
+ options = options || {}
236
+ send('OAUTH_REQUEST', {
237
+ provider: provider,
238
+ endpoint: endpoint,
239
+ method: options.method || 'GET',
240
+ body: options.body,
241
+ headers: options.headers,
242
+ requiredScopes: options.requiredScopes || [],
243
+ requestId: id
244
+ })
245
+ })
246
+ }
247
+
248
+ /**
249
+ * Get the scopes granted for a connected OAuth provider
250
+ * Returns an empty array if not connected
251
+ *
252
+ * @param {string} provider - OAuth provider
253
+ * @returns {Promise<string[]>} Array of granted scopes
254
+ */
255
+ function oauthGetScopes(provider) {
256
+ return new Promise(function (resolve, reject) {
257
+ var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
258
+ var handler = function (e) {
259
+ if (e.source !== window.parent) return
260
+ var d = e.data
261
+ if (!d || d.source !== 'MyWallpaperHost') return
262
+ if (d.type === 'OAUTH_SCOPES_RESPONSE' && d.payload.requestId === id) {
263
+ window.removeEventListener('message', handler)
264
+ if (d.payload.success) {
265
+ resolve(d.payload.scopes || [])
266
+ } else {
267
+ reject(new Error(d.payload.error || 'Failed to get scopes'))
268
+ }
269
+ }
270
+ }
271
+ window.addEventListener('message', handler)
272
+ // 10s timeout
273
+ setTimeout(function () {
274
+ window.removeEventListener('message', handler)
275
+ resolve([]) // Return empty array on timeout (not connected)
276
+ }, 10000)
277
+ send('OAUTH_GET_SCOPES', { provider: provider, requestId: id })
278
+ })
279
+ }
280
+
281
+ /**
282
+ * Request re-authorization with additional scopes
283
+ * Opens the OAuth flow with new scopes while preserving existing ones
284
+ *
285
+ * @param {string} provider - OAuth provider
286
+ * @param {string[]} scopes - Additional scopes to request
287
+ * @param {string} reason - Reason shown to user
288
+ * @returns {Promise<boolean>} True if re-auth was successful
289
+ */
290
+ function oauthRequestScopes(provider, scopes, reason) {
291
+ return new Promise(function (resolve) {
292
+ var requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`
293
+ pendingOAuthScopes.set(requestId, resolve)
294
+ // 60s timeout - user needs time to complete OAuth flow
295
+ setTimeout(function () {
296
+ if (pendingOAuthScopes.has(requestId)) {
297
+ pendingOAuthScopes.delete(requestId)
298
+ resolve(false)
299
+ }
300
+ }, 60000)
301
+ send('OAUTH_REQUEST_SCOPES', { provider: provider, scopes: scopes, reason: reason, requestId: requestId })
302
+ })
303
+ }
304
+
305
+ window.addEventListener('message', onMessage)
306
+
307
+ window.MyWallpaper = {
308
+ get config() { return config },
309
+ layerId: layerId,
310
+ version: '2.5',
311
+
312
+ onSettingsChange: function (fn) { if (typeof fn === 'function') settingsCallbacks.push(fn) },
313
+
314
+ onEvent: function (event, fn) {
315
+ if (typeof fn !== 'function') return
316
+ if (!eventCallbacks.has(event)) eventCallbacks.set(event, new Set())
317
+ eventCallbacks.get(event).add(fn)
318
+ },
319
+
320
+ offEvent: function (event, fn) {
321
+ var cbs = eventCallbacks.get(event)
322
+ if (cbs) cbs.delete(fn)
323
+ },
324
+
325
+ onMount: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.mount.push(fn) },
326
+ onUnmount: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.unmount.push(fn) },
327
+ onPause: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.pause.push(fn) },
328
+ onResume: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.resume.push(fn) },
329
+
330
+ ready: function (opts) {
331
+ if (isReady) return
332
+ isReady = true
333
+ opts = opts || {}
334
+ send('ADDON_READY', {
335
+ version: '2.5',
336
+ capabilities: opts.capabilities || [],
337
+ subscribedEvents: opts.subscribedEvents || []
338
+ })
339
+ },
340
+
341
+ requestPermission: function (perm, reason) {
342
+ return new Promise(function (resolve) {
343
+ var requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`
344
+ pendingPermissions.set(requestId, resolve)
345
+ // 30s timeout - user needs time to see and respond to permission modal
346
+ setTimeout(function () {
347
+ if (pendingPermissions.has(requestId)) {
348
+ pendingPermissions.delete(requestId)
349
+ resolve(false) // Timeout = denied
350
+ }
351
+ }, 30000)
352
+ send('REQUEST_PERMISSION', { permission: perm, reason: reason, requestId: requestId })
353
+ })
354
+ },
355
+
356
+ renderComplete: function () {
357
+ if (isRenderComplete) return
358
+ isRenderComplete = true
359
+ var renderTimeMs = Math.round(performance.now())
360
+ send('RENDER_COMPLETE', { renderTimeMs: renderTimeMs })
361
+ },
362
+
363
+ storage: {
364
+ get: function (k) { return storageOp('get', k) },
365
+ set: function (k, v) { return storageOp('set', k, v) },
366
+ delete: function (k) { return storageOp('delete', k) },
367
+ clear: function () { return storageOp('clear') },
368
+ keys: function () { return storageOp('keys') },
369
+ size: function () { return storageOp('size') }
370
+ },
371
+
372
+ /**
373
+ * Network API - Secure proxy for HTTP requests
374
+ * All requests go through the host which validates domain whitelist
375
+ *
376
+ * @example
377
+ * // manifest.json: "permissions": { "network": { "domains": ["api.weather.com"] } }
378
+ * const response = await MyWallpaper.network.fetch('https://api.weather.com/current')
379
+ * if (response.ok) console.log(response.data)
380
+ */
381
+ network: {
382
+ fetch: networkFetch
383
+ },
384
+
385
+ /**
386
+ * OAuth API - Secure authenticated API calls to OAuth providers
387
+ * Tokens are stored securely on the server - never exposed to addons
388
+ *
389
+ * Supported providers: github, google, discord, spotify, twitch
390
+ *
391
+ * @example
392
+ * // First request permission
393
+ * const granted = await MyWallpaper.requestPermission('oauth:github', 'To display your GitHub profile')
394
+ * if (granted) {
395
+ * // Make authenticated API request
396
+ * const response = await MyWallpaper.oauth.request('github', '/user')
397
+ * if (response.ok) console.log('GitHub user:', response.data)
398
+ *
399
+ * // List repositories (with required scopes)
400
+ * const repos = await MyWallpaper.oauth.request('github', '/user/repos', {
401
+ * requiredScopes: ['repo'] // Will return scope error if missing
402
+ * })
403
+ * if (repos.error === 'insufficient_scopes') {
404
+ * // Request additional scopes
405
+ * const granted = await MyWallpaper.oauth.requestScopes('github', repos.missingScopes, 'Access private repos')
406
+ * if (granted) {
407
+ * // Retry request
408
+ * const repos = await MyWallpaper.oauth.request('github', '/user/repos', { requiredScopes: ['repo'] })
409
+ * }
410
+ * }
411
+ * }
412
+ */
413
+ oauth: {
414
+ request: oauthRequest,
415
+ getScopes: oauthGetScopes,
416
+ requestScopes: oauthRequestScopes,
417
+ isConnected: function (provider) {
418
+ // Check connection by getting scopes - if scopes array is returned, provider is connected
419
+ return oauthGetScopes(provider)
420
+ .then(function (scopes) { return scopes.length > 0 })
421
+ .catch(function () { return false })
422
+ }
423
+ },
424
+
425
+ /**
426
+ * File Access API - Request access to user-uploaded files
427
+ * Files are not sent automatically - the addon must request access
428
+ * A modal is shown to the user EVERY TIME (not persisted)
429
+ *
430
+ * @example
431
+ * const videoRef = MyWallpaper.config.backgroundVideo
432
+ * if (MyWallpaper.files.isFileReference(videoRef)) {
433
+ * const result = await MyWallpaper.files.request('backgroundVideo')
434
+ * if (result.granted) {
435
+ * video.src = result.blobUrl
436
+ * }
437
+ * }
438
+ */
439
+ files: {
440
+ /**
441
+ * Check if a value is a file reference placeholder
442
+ * @param {unknown} value - Any value from config
443
+ * @returns {boolean} true if this is a FileReference
444
+ */
445
+ isFileReference: function (value) {
446
+ return value !== null &&
447
+ typeof value === 'object' &&
448
+ value.__type === 'file-reference' &&
449
+ typeof value.settingKey === 'string'
450
+ },
451
+
452
+ /**
453
+ * Request access to a file from the user
454
+ * Shows a modal asking for permission (every time)
455
+ * @param {string} settingKey - The setting key containing the file reference
456
+ * @returns {Promise<{granted: boolean, blobUrl?: string, error?: string}>}
457
+ */
458
+ request: function (settingKey) {
459
+ return new Promise(function (resolve) {
460
+ var requestId = Date.now() + '-' + Math.random().toString(36).slice(2)
461
+ pendingFileAccess.set(requestId, { resolve: resolve })
462
+ // 60s timeout - user needs time to respond to modal
463
+ setTimeout(function () {
464
+ if (pendingFileAccess.has(requestId)) {
465
+ pendingFileAccess.delete(requestId)
466
+ resolve({ granted: false, error: 'Request timeout' })
467
+ }
468
+ }, 60000)
469
+ send('FILE_ACCESS_REQUEST', { settingKey: settingKey, requestId: requestId })
470
+ })
471
+ },
472
+
473
+ /**
474
+ * Revoke access to a previously granted file
475
+ * The blob URL will be invalidated
476
+ * @param {string} settingKey - The setting key to revoke access for
477
+ */
478
+ revoke: function (settingKey) {
479
+ var blobUrl = grantedBlobUrls.get(settingKey)
480
+ if (blobUrl) {
481
+ URL.revokeObjectURL(blobUrl)
482
+ grantedBlobUrls.delete(settingKey)
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ console.warn('[MyWallpaper] SDK v2.5 ready:', layerId)
489
+ })()
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Network Sandbox Script - SECURITY LAYER
3
+ *
4
+ * This script MUST be injected FIRST (before addon-client.js)
5
+ * It saves original fetch/XHR for the SDK, then blocks them for addon code.
6
+ *
7
+ * Flow:
8
+ * 1. Save original fetch/XMLHttpRequest to __MYWALLPAPER_ORIGINAL_*
9
+ * 2. After addon-client.js loads, it will use these for the proxy
10
+ * 3. Replace window.fetch and XMLHttpRequest with blocked versions
11
+ * 4. Addon code must use MyWallpaper.network.fetch() instead
12
+ */
13
+ ;(function() {
14
+ 'use strict';
15
+
16
+ // ============================================================================
17
+ // STEP 1: Save original network APIs for SDK internal use
18
+ // ============================================================================
19
+
20
+ // These will be used by the SDK's network proxy
21
+ window.__MYWALLPAPER_ORIGINAL_FETCH__ = window.fetch;
22
+ window.__MYWALLPAPER_ORIGINAL_XHR__ = window.XMLHttpRequest;
23
+
24
+ // ============================================================================
25
+ // STEP 2: Create blocked versions with helpful error messages
26
+ // ============================================================================
27
+
28
+ const BLOCKED_MESSAGE =
29
+ '[MyWallpaper Security] Direct network access is blocked for security.\n' +
30
+ 'Use window.MyWallpaper.network.fetch(url, options) instead.\n\n' +
31
+ 'Example:\n' +
32
+ ' const response = await MyWallpaper.network.fetch("https://api.example.com/data");\n' +
33
+ ' if (response.ok) console.log(response.data);\n\n' +
34
+ 'Make sure your domain is declared in manifest.json:\n' +
35
+ ' "permissions": { "network": { "domains": ["api.example.com"] } }';
36
+
37
+ // Blocked fetch function
38
+ function blockedFetch() {
39
+ console.error(BLOCKED_MESSAGE);
40
+ return Promise.reject(new Error(BLOCKED_MESSAGE));
41
+ }
42
+
43
+ // Blocked XMLHttpRequest class
44
+ class BlockedXMLHttpRequest {
45
+ constructor() {
46
+ console.error(BLOCKED_MESSAGE);
47
+ throw new Error(BLOCKED_MESSAGE);
48
+ }
49
+ }
50
+
51
+ // Make BlockedXMLHttpRequest look like the real XMLHttpRequest
52
+ Object.defineProperties(BlockedXMLHttpRequest, {
53
+ UNSENT: { value: 0 },
54
+ OPENED: { value: 1 },
55
+ HEADERS_RECEIVED: { value: 2 },
56
+ LOADING: { value: 3 },
57
+ DONE: { value: 4 },
58
+ });
59
+
60
+ // ============================================================================
61
+ // STEP 3: Replace global APIs with blocked versions
62
+ // ============================================================================
63
+
64
+ // Override fetch - make it non-configurable to prevent bypass attempts
65
+ Object.defineProperty(window, 'fetch', {
66
+ value: blockedFetch,
67
+ writable: false,
68
+ configurable: false,
69
+ enumerable: true
70
+ });
71
+
72
+ // Override XMLHttpRequest - make it non-configurable
73
+ Object.defineProperty(window, 'XMLHttpRequest', {
74
+ value: BlockedXMLHttpRequest,
75
+ writable: false,
76
+ configurable: false,
77
+ enumerable: true
78
+ });
79
+
80
+ // ============================================================================
81
+ // STEP 4: Also block in common bypass locations
82
+ // ============================================================================
83
+
84
+ // Block on globalThis (modern JS)
85
+ if (typeof globalThis !== 'undefined' && globalThis !== window) {
86
+ try {
87
+ Object.defineProperty(globalThis, 'fetch', {
88
+ value: blockedFetch,
89
+ writable: false,
90
+ configurable: false
91
+ });
92
+ Object.defineProperty(globalThis, 'XMLHttpRequest', {
93
+ value: BlockedXMLHttpRequest,
94
+ writable: false,
95
+ configurable: false
96
+ });
97
+ } catch (e) {
98
+ // Ignore if already defined
99
+ }
100
+ }
101
+
102
+ // Prevent restoration via prototype manipulation
103
+ try {
104
+ Object.freeze(Object.getPrototypeOf(blockedFetch));
105
+ } catch (e) {
106
+ // Ignore
107
+ }
108
+
109
+ // ============================================================================
110
+ // STEP 5: Block EventSource and WebSocket (optional - for future)
111
+ // ============================================================================
112
+
113
+ // EventSource (Server-Sent Events)
114
+ const BlockedEventSource = function() {
115
+ console.error('[MyWallpaper Security] EventSource is blocked. Use MyWallpaper.network.fetch() for data.');
116
+ throw new Error('EventSource is blocked for security. Use MyWallpaper.network.fetch()');
117
+ };
118
+
119
+ Object.defineProperty(window, 'EventSource', {
120
+ value: BlockedEventSource,
121
+ writable: false,
122
+ configurable: false,
123
+ enumerable: true
124
+ });
125
+
126
+ // WebSocket - block with helpful message
127
+ const OriginalWebSocket = window.WebSocket;
128
+ const BlockedWebSocket = function() {
129
+ console.error('[MyWallpaper Security] WebSocket is blocked. Real-time features are not supported in addons.');
130
+ throw new Error('WebSocket is blocked for security in addons');
131
+ };
132
+
133
+ // Copy static properties
134
+ if (OriginalWebSocket) {
135
+ BlockedWebSocket.CONNECTING = 0;
136
+ BlockedWebSocket.OPEN = 1;
137
+ BlockedWebSocket.CLOSING = 2;
138
+ BlockedWebSocket.CLOSED = 3;
139
+ }
140
+
141
+ Object.defineProperty(window, 'WebSocket', {
142
+ value: BlockedWebSocket,
143
+ writable: false,
144
+ configurable: false,
145
+ enumerable: true
146
+ });
147
+
148
+ // ============================================================================
149
+ // STEP 6: Log security initialization
150
+ // ============================================================================
151
+
152
+ console.warn('[MyWallpaper Security] Network sandbox active. Use MyWallpaper.network.fetch() for network requests.');
153
+ })();