@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 +3 -2
- package/src/runtime/addon-client.js +489 -0
- package/src/runtime/network-sandbox.js +153 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mywallpaper/addon-sdk",
|
|
3
|
-
"version": "2.
|
|
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
|
+
})();
|