safari-pilot 0.1.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.
Files changed (143) hide show
  1. package/.claude-plugin/plugin.json +35 -0
  2. package/.mcp.json +11 -0
  3. package/LICENSE +21 -0
  4. package/README.md +324 -0
  5. package/bin/.gitkeep +0 -0
  6. package/bin/Safari Pilot.app/Contents/CodeResources +0 -0
  7. package/bin/Safari Pilot.app/Contents/Info.plist +58 -0
  8. package/bin/Safari Pilot.app/Contents/MacOS/Safari Pilot +0 -0
  9. package/bin/Safari Pilot.app/Contents/PkgInfo +1 -0
  10. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Info.plist +55 -0
  11. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/MacOS/Safari Pilot Extension +0 -0
  12. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/background.js +294 -0
  13. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-isolated.js +80 -0
  14. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-main.js +310 -0
  15. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-128.png +0 -0
  16. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-48.png +0 -0
  17. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-96.png +0 -0
  18. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/manifest.json +39 -0
  19. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/_CodeSignature/CodeResources +194 -0
  20. package/bin/Safari Pilot.app/Contents/Resources/AppIcon.icns +0 -0
  21. package/bin/Safari Pilot.app/Contents/Resources/Assets.car +0 -0
  22. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.html +19 -0
  23. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist +0 -0
  24. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib +0 -0
  25. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib +0 -0
  26. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib +0 -0
  27. package/bin/Safari Pilot.app/Contents/Resources/Icon.png +0 -0
  28. package/bin/Safari Pilot.app/Contents/Resources/Script.js +22 -0
  29. package/bin/Safari Pilot.app/Contents/Resources/Style.css +45 -0
  30. package/bin/Safari Pilot.app/Contents/_CodeSignature/CodeResources +236 -0
  31. package/bin/Safari Pilot.zip +0 -0
  32. package/bin/SafariPilotd +0 -0
  33. package/dist/engine-selector.d.ts +10 -0
  34. package/dist/engine-selector.js +55 -0
  35. package/dist/engine-selector.js.map +1 -0
  36. package/dist/engines/applescript.d.ts +53 -0
  37. package/dist/engines/applescript.js +290 -0
  38. package/dist/engines/applescript.js.map +1 -0
  39. package/dist/engines/daemon.d.ts +19 -0
  40. package/dist/engines/daemon.js +187 -0
  41. package/dist/engines/daemon.js.map +1 -0
  42. package/dist/engines/engine.d.ts +15 -0
  43. package/dist/engines/engine.js +42 -0
  44. package/dist/engines/engine.js.map +1 -0
  45. package/dist/engines/extension.d.ts +34 -0
  46. package/dist/engines/extension.js +66 -0
  47. package/dist/engines/extension.js.map +1 -0
  48. package/dist/errors.d.ts +128 -0
  49. package/dist/errors.js +250 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +11 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/security/audit-log.d.ts +23 -0
  55. package/dist/security/audit-log.js +68 -0
  56. package/dist/security/audit-log.js.map +1 -0
  57. package/dist/security/circuit-breaker.d.ts +29 -0
  58. package/dist/security/circuit-breaker.js +114 -0
  59. package/dist/security/circuit-breaker.js.map +1 -0
  60. package/dist/security/domain-policy.d.ts +29 -0
  61. package/dist/security/domain-policy.js +96 -0
  62. package/dist/security/domain-policy.js.map +1 -0
  63. package/dist/security/human-approval.d.ts +20 -0
  64. package/dist/security/human-approval.js +150 -0
  65. package/dist/security/human-approval.js.map +1 -0
  66. package/dist/security/idpi-scanner.d.ts +20 -0
  67. package/dist/security/idpi-scanner.js +102 -0
  68. package/dist/security/idpi-scanner.js.map +1 -0
  69. package/dist/security/kill-switch.d.ts +51 -0
  70. package/dist/security/kill-switch.js +103 -0
  71. package/dist/security/kill-switch.js.map +1 -0
  72. package/dist/security/rate-limiter.d.ts +30 -0
  73. package/dist/security/rate-limiter.js +70 -0
  74. package/dist/security/rate-limiter.js.map +1 -0
  75. package/dist/security/screenshot-redaction.d.ts +42 -0
  76. package/dist/security/screenshot-redaction.js +134 -0
  77. package/dist/security/screenshot-redaction.js.map +1 -0
  78. package/dist/security/tab-ownership.d.ts +46 -0
  79. package/dist/security/tab-ownership.js +85 -0
  80. package/dist/security/tab-ownership.js.map +1 -0
  81. package/dist/server.d.ts +53 -0
  82. package/dist/server.js +347 -0
  83. package/dist/server.js.map +1 -0
  84. package/dist/tools/clipboard.d.ts +15 -0
  85. package/dist/tools/clipboard.js +128 -0
  86. package/dist/tools/clipboard.js.map +1 -0
  87. package/dist/tools/compound.d.ts +68 -0
  88. package/dist/tools/compound.js +491 -0
  89. package/dist/tools/compound.js.map +1 -0
  90. package/dist/tools/extraction.d.ts +26 -0
  91. package/dist/tools/extraction.js +414 -0
  92. package/dist/tools/extraction.js.map +1 -0
  93. package/dist/tools/frames.d.ts +22 -0
  94. package/dist/tools/frames.js +165 -0
  95. package/dist/tools/frames.js.map +1 -0
  96. package/dist/tools/interaction.d.ts +30 -0
  97. package/dist/tools/interaction.js +651 -0
  98. package/dist/tools/interaction.js.map +1 -0
  99. package/dist/tools/navigation.d.ts +41 -0
  100. package/dist/tools/navigation.js +316 -0
  101. package/dist/tools/navigation.js.map +1 -0
  102. package/dist/tools/network.d.ts +27 -0
  103. package/dist/tools/network.js +721 -0
  104. package/dist/tools/network.js.map +1 -0
  105. package/dist/tools/performance.d.ts +16 -0
  106. package/dist/tools/performance.js +240 -0
  107. package/dist/tools/performance.js.map +1 -0
  108. package/dist/tools/permissions.d.ts +25 -0
  109. package/dist/tools/permissions.js +308 -0
  110. package/dist/tools/permissions.js.map +1 -0
  111. package/dist/tools/service-workers.d.ts +15 -0
  112. package/dist/tools/service-workers.js +136 -0
  113. package/dist/tools/service-workers.js.map +1 -0
  114. package/dist/tools/shadow.d.ts +21 -0
  115. package/dist/tools/shadow.js +126 -0
  116. package/dist/tools/shadow.js.map +1 -0
  117. package/dist/tools/storage.d.ts +30 -0
  118. package/dist/tools/storage.js +679 -0
  119. package/dist/tools/storage.js.map +1 -0
  120. package/dist/tools/structured-extraction.d.ts +22 -0
  121. package/dist/tools/structured-extraction.js +433 -0
  122. package/dist/tools/structured-extraction.js.map +1 -0
  123. package/dist/tools/wait.d.ts +18 -0
  124. package/dist/tools/wait.js +182 -0
  125. package/dist/tools/wait.js.map +1 -0
  126. package/dist/types.d.ts +85 -0
  127. package/dist/types.js +2 -0
  128. package/dist/types.js.map +1 -0
  129. package/extension/background.js +294 -0
  130. package/extension/content-isolated.js +80 -0
  131. package/extension/content-main.js +310 -0
  132. package/extension/icons/icon-128.png +0 -0
  133. package/extension/icons/icon-48.png +0 -0
  134. package/extension/icons/icon-96.png +0 -0
  135. package/extension/manifest.json +39 -0
  136. package/hooks/session-end.sh +67 -0
  137. package/hooks/session-start.sh +66 -0
  138. package/package.json +46 -0
  139. package/scripts/build-extension.sh +135 -0
  140. package/scripts/postinstall.sh +91 -0
  141. package/scripts/preuninstall.sh +25 -0
  142. package/scripts/update-daemon.sh +62 -0
  143. package/skills/safari-pilot/SKILL.md +157 -0
@@ -0,0 +1,679 @@
1
+ export class StorageTools {
2
+ engine;
3
+ handlers = new Map();
4
+ constructor(engine) {
5
+ this.engine = engine;
6
+ this.registerHandlers();
7
+ }
8
+ registerHandlers() {
9
+ this.handlers.set('safari_get_cookies', this.handleGetCookies.bind(this));
10
+ this.handlers.set('safari_set_cookie', this.handleSetCookie.bind(this));
11
+ this.handlers.set('safari_delete_cookie', this.handleDeleteCookie.bind(this));
12
+ this.handlers.set('safari_storage_state_export', this.handleStorageStateExport.bind(this));
13
+ this.handlers.set('safari_storage_state_import', this.handleStorageStateImport.bind(this));
14
+ this.handlers.set('safari_local_storage_get', this.handleLocalStorageGet.bind(this));
15
+ this.handlers.set('safari_local_storage_set', this.handleLocalStorageSet.bind(this));
16
+ this.handlers.set('safari_session_storage_get', this.handleSessionStorageGet.bind(this));
17
+ this.handlers.set('safari_session_storage_set', this.handleSessionStorageSet.bind(this));
18
+ this.handlers.set('safari_idb_list', this.handleIdbList.bind(this));
19
+ this.handlers.set('safari_idb_get', this.handleIdbGet.bind(this));
20
+ }
21
+ // ── Public API ──────────────────────────────────────────────────────────────
22
+ getDefinitions() {
23
+ return [
24
+ {
25
+ name: 'safari_get_cookies',
26
+ description: 'Get cookies accessible via document.cookie for the current page. ' +
27
+ 'Returns name, value, domain, path, and security flags. ' +
28
+ 'Note: httpOnly cookies are not accessible via JS — use the extension engine (Phase 3) for full cookie access. ' +
29
+ 'Optionally filter by domain substring.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ tabUrl: { type: 'string', description: 'URL of the tab to read cookies from' },
34
+ domain: { type: 'string', description: 'Filter cookies to those matching this domain substring' },
35
+ },
36
+ required: [],
37
+ },
38
+ requirements: {},
39
+ },
40
+ {
41
+ name: 'safari_set_cookie',
42
+ description: 'Set a cookie on the current page via document.cookie. ' +
43
+ 'Supports name, value, domain, path, expiry, and security flags. ' +
44
+ 'httpOnly cookies cannot be set via JS — requires extension engine (Phase 3).',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ tabUrl: { type: 'string', description: 'URL of the tab to set the cookie on' },
49
+ name: { type: 'string', description: 'Cookie name' },
50
+ value: { type: 'string', description: 'Cookie value' },
51
+ domain: { type: 'string', description: 'Cookie domain (defaults to current domain)' },
52
+ path: { type: 'string', description: 'Cookie path', default: '/' },
53
+ expires: {
54
+ type: 'string',
55
+ description: 'Expiry as ISO date string or number of seconds from now (e.g. "3600" for 1 hour)',
56
+ },
57
+ httpOnly: {
58
+ type: 'boolean',
59
+ description: 'Set httpOnly flag (note: has no effect via document.cookie — requires extension)',
60
+ default: false,
61
+ },
62
+ secure: { type: 'boolean', description: 'Set Secure flag', default: false },
63
+ sameSite: {
64
+ type: 'string',
65
+ enum: ['strict', 'lax', 'none'],
66
+ description: 'SameSite policy',
67
+ default: 'lax',
68
+ },
69
+ },
70
+ required: ['tabUrl', 'name', 'value'],
71
+ },
72
+ requirements: {},
73
+ },
74
+ {
75
+ name: 'safari_delete_cookie',
76
+ description: 'Delete a cookie by name on the current page. ' +
77
+ 'Optionally specify domain and path to target the exact cookie. ' +
78
+ 'Works by setting the expiry to the past via document.cookie.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ tabUrl: { type: 'string', description: 'URL of the tab' },
83
+ name: { type: 'string', description: 'Cookie name to delete' },
84
+ domain: { type: 'string', description: 'Cookie domain (optional, helps target the right cookie)' },
85
+ path: { type: 'string', description: 'Cookie path', default: '/' },
86
+ },
87
+ required: ['tabUrl', 'name'],
88
+ },
89
+ requirements: {},
90
+ },
91
+ {
92
+ name: 'safari_storage_state_export',
93
+ description: 'Export the full storage state of the current page: cookies (via document.cookie), ' +
94
+ 'localStorage, and sessionStorage. Useful for saving authenticated session state ' +
95
+ 'to restore later with safari_storage_state_import.',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ tabUrl: { type: 'string', description: 'URL of the tab to export state from' },
100
+ },
101
+ required: ['tabUrl'],
102
+ },
103
+ requirements: {},
104
+ },
105
+ {
106
+ name: 'safari_storage_state_import',
107
+ description: 'Import a previously exported storage state into the current page. ' +
108
+ 'Restores cookies (via document.cookie), localStorage, and sessionStorage. ' +
109
+ 'Typically used to restore an authenticated session without re-logging in. ' +
110
+ 'Pass the state object returned by safari_storage_state_export.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ tabUrl: { type: 'string', description: 'URL of the tab to import state into' },
115
+ state: {
116
+ type: 'object',
117
+ description: 'Storage state object from safari_storage_state_export',
118
+ properties: {
119
+ cookies: { type: 'array', items: { type: 'object' } },
120
+ localStorage: { type: 'object' },
121
+ sessionStorage: { type: 'object' },
122
+ },
123
+ },
124
+ },
125
+ required: ['tabUrl', 'state'],
126
+ },
127
+ requirements: {},
128
+ },
129
+ {
130
+ name: 'safari_local_storage_get',
131
+ description: 'Get a single localStorage value by key from the current page.',
132
+ inputSchema: {
133
+ type: 'object',
134
+ properties: {
135
+ tabUrl: { type: 'string', description: 'URL of the tab' },
136
+ key: { type: 'string', description: 'localStorage key to retrieve' },
137
+ },
138
+ required: ['tabUrl', 'key'],
139
+ },
140
+ requirements: {},
141
+ },
142
+ {
143
+ name: 'safari_local_storage_set',
144
+ description: 'Set a localStorage key to the given value on the current page.',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ tabUrl: { type: 'string', description: 'URL of the tab' },
149
+ key: { type: 'string', description: 'localStorage key to set' },
150
+ value: { type: 'string', description: 'Value to store (will be coerced to string by localStorage)' },
151
+ },
152
+ required: ['tabUrl', 'key', 'value'],
153
+ },
154
+ requirements: {},
155
+ },
156
+ {
157
+ name: 'safari_session_storage_get',
158
+ description: 'Get a single sessionStorage value by key from the current page.',
159
+ inputSchema: {
160
+ type: 'object',
161
+ properties: {
162
+ tabUrl: { type: 'string', description: 'URL of the tab' },
163
+ key: { type: 'string', description: 'sessionStorage key to retrieve' },
164
+ },
165
+ required: ['tabUrl', 'key'],
166
+ },
167
+ requirements: {},
168
+ },
169
+ {
170
+ name: 'safari_session_storage_set',
171
+ description: 'Set a sessionStorage key to the given value on the current page.',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ tabUrl: { type: 'string', description: 'URL of the tab' },
176
+ key: { type: 'string', description: 'sessionStorage key to set' },
177
+ value: { type: 'string', description: 'Value to store' },
178
+ },
179
+ required: ['tabUrl', 'key', 'value'],
180
+ },
181
+ requirements: {},
182
+ },
183
+ {
184
+ name: 'safari_idb_list',
185
+ description: 'List all IndexedDB databases available on the current origin. ' +
186
+ 'Returns an array of database descriptors with name and version.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ tabUrl: { type: 'string', description: 'URL of the tab' },
191
+ },
192
+ required: ['tabUrl'],
193
+ },
194
+ requirements: {},
195
+ },
196
+ {
197
+ name: 'safari_idb_get',
198
+ description: 'Read records from an IndexedDB object store. ' +
199
+ 'Optionally filter with a key range query. ' +
200
+ 'Returns up to 100 records by default.',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ tabUrl: { type: 'string', description: 'URL of the tab' },
205
+ database: { type: 'string', description: 'IndexedDB database name' },
206
+ store: { type: 'string', description: 'Object store name within the database' },
207
+ query: {
208
+ type: 'object',
209
+ description: 'Optional key range query',
210
+ properties: {
211
+ lower: { description: 'Lower bound of key range (any serializable value)' },
212
+ upper: { description: 'Upper bound of key range (any serializable value)' },
213
+ lowerOpen: { type: 'boolean', description: 'Exclude lower bound', default: false },
214
+ upperOpen: { type: 'boolean', description: 'Exclude upper bound', default: false },
215
+ only: { description: 'Exact key to retrieve (overrides lower/upper)' },
216
+ },
217
+ },
218
+ limit: { type: 'number', description: 'Maximum records to return', default: 100 },
219
+ },
220
+ required: ['tabUrl', 'database', 'store'],
221
+ },
222
+ requirements: {},
223
+ },
224
+ ];
225
+ }
226
+ getHandler(name) {
227
+ return this.handlers.get(name);
228
+ }
229
+ // ── Handlers ────────────────────────────────────────────────────────────────
230
+ async handleGetCookies(params) {
231
+ const start = Date.now();
232
+ const tabUrl = params['tabUrl'];
233
+ const domain = params['domain'];
234
+ const escapedDomain = domain ? domain.replace(/'/g, "\\'") : '';
235
+ const js = `
236
+ var rawCookies = document.cookie;
237
+ var cookies = [];
238
+
239
+ if (rawCookies) {
240
+ rawCookies.split('; ').forEach(function(pair) {
241
+ var idx = pair.indexOf('=');
242
+ var name = idx !== -1 ? pair.slice(0, idx) : pair;
243
+ var value = idx !== -1 ? pair.slice(idx + 1) : '';
244
+
245
+ // Infer basic info from current page context
246
+ var cookie = {
247
+ name: name.trim(),
248
+ value: value,
249
+ domain: window.location.hostname,
250
+ path: '/',
251
+ httpOnly: false,
252
+ secure: window.location.protocol === 'https:',
253
+ sameSite: 'lax',
254
+ };
255
+ cookies.push(cookie);
256
+ });
257
+ }
258
+
259
+ var domainFilter = ${domain ? `'${escapedDomain}'` : 'null'};
260
+ if (domainFilter) {
261
+ cookies = cookies.filter(function(c) { return c.domain.indexOf(domainFilter) !== -1; });
262
+ }
263
+
264
+ return { cookies: cookies, count: cookies.length };
265
+ `;
266
+ const result = await this.engine.executeJsInTab(tabUrl ?? '', js);
267
+ if (!result.ok)
268
+ throw new Error(result.error?.message ?? 'Get cookies failed');
269
+ return this.makeResponse(result.value ? JSON.parse(result.value) : { cookies: [], count: 0 }, Date.now() - start);
270
+ }
271
+ async handleSetCookie(params) {
272
+ const start = Date.now();
273
+ const tabUrl = params['tabUrl'];
274
+ const name = params['name'];
275
+ const value = params['value'];
276
+ const domain = params['domain'];
277
+ const path = params['path'] ?? '/';
278
+ const expires = params['expires'];
279
+ const secure = params['secure'] === true;
280
+ const sameSite = params['sameSite'] ?? 'lax';
281
+ const escapedName = name.replace(/'/g, "\\'");
282
+ const escapedValue = value.replace(/'/g, "\\'");
283
+ const escapedDomain = domain ? domain.replace(/'/g, "\\'") : '';
284
+ const escapedPath = path.replace(/'/g, "\\'");
285
+ const escapedSameSite = sameSite.replace(/'/g, "\\'");
286
+ const js = `
287
+ var cookieName = '${escapedName}';
288
+ var cookieValue = '${escapedValue}';
289
+ var cookieDomain = ${domain ? `'${escapedDomain}'` : 'null'};
290
+ var cookiePath = '${escapedPath}';
291
+ var cookieSecure = ${secure};
292
+ var cookieSameSite = '${escapedSameSite}';
293
+ var expiresParam = ${expires ? `'${expires.replace(/'/g, "\\'")}'` : 'null'};
294
+
295
+ var parts = [cookieName + '=' + cookieValue];
296
+
297
+ if (cookieDomain) parts.push('domain=' + cookieDomain);
298
+ parts.push('path=' + cookiePath);
299
+
300
+ if (expiresParam) {
301
+ var expDate;
302
+ if (/^\\d+$/.test(expiresParam)) {
303
+ // Seconds from now
304
+ expDate = new Date(Date.now() + parseInt(expiresParam, 10) * 1000);
305
+ } else {
306
+ // ISO date string
307
+ expDate = new Date(expiresParam);
308
+ }
309
+ parts.push('expires=' + expDate.toUTCString());
310
+ }
311
+
312
+ if (cookieSecure) parts.push('Secure');
313
+ parts.push('SameSite=' + cookieSameSite.charAt(0).toUpperCase() + cookieSameSite.slice(1));
314
+
315
+ document.cookie = parts.join('; ');
316
+
317
+ // Verify the cookie was set by reading it back
318
+ var set = document.cookie.split('; ').some(function(pair) {
319
+ return pair.split('=')[0].trim() === cookieName;
320
+ });
321
+
322
+ return { set: set, name: cookieName, cookie: parts.join('; ') };
323
+ `;
324
+ const result = await this.engine.executeJsInTab(tabUrl, js);
325
+ if (!result.ok)
326
+ throw new Error(result.error?.message ?? 'Set cookie failed');
327
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
328
+ }
329
+ async handleDeleteCookie(params) {
330
+ const start = Date.now();
331
+ const tabUrl = params['tabUrl'];
332
+ const name = params['name'];
333
+ const domain = params['domain'];
334
+ const path = params['path'] ?? '/';
335
+ const escapedName = name.replace(/'/g, "\\'");
336
+ const escapedDomain = domain ? domain.replace(/'/g, "\\'") : '';
337
+ const escapedPath = path.replace(/'/g, "\\'");
338
+ const js = `
339
+ var cookieName = '${escapedName}';
340
+ var cookieDomain = ${domain ? `'${escapedDomain}'` : 'null'};
341
+ var cookiePath = '${escapedPath}';
342
+
343
+ // Check if the cookie exists before deleting
344
+ var existed = document.cookie.split('; ').some(function(pair) {
345
+ return pair.split('=')[0].trim() === cookieName;
346
+ });
347
+
348
+ // Delete by setting expiry to the past
349
+ var parts = [cookieName + '='];
350
+ if (cookieDomain) parts.push('domain=' + cookieDomain);
351
+ parts.push('path=' + cookiePath);
352
+ parts.push('expires=Thu, 01 Jan 1970 00:00:00 GMT');
353
+
354
+ document.cookie = parts.join('; ');
355
+
356
+ // Verify deletion
357
+ var stillExists = document.cookie.split('; ').some(function(pair) {
358
+ return pair.split('=')[0].trim() === cookieName;
359
+ });
360
+
361
+ return { deleted: existed && !stillExists, existed: existed, name: cookieName };
362
+ `;
363
+ const result = await this.engine.executeJsInTab(tabUrl, js);
364
+ if (!result.ok)
365
+ throw new Error(result.error?.message ?? 'Delete cookie failed');
366
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
367
+ }
368
+ async handleStorageStateExport(params) {
369
+ const start = Date.now();
370
+ const tabUrl = params['tabUrl'];
371
+ const js = `
372
+ // Export cookies
373
+ var cookies = [];
374
+ var rawCookies = document.cookie;
375
+ if (rawCookies) {
376
+ rawCookies.split('; ').forEach(function(pair) {
377
+ var idx = pair.indexOf('=');
378
+ var name = idx !== -1 ? pair.slice(0, idx).trim() : pair.trim();
379
+ var value = idx !== -1 ? pair.slice(idx + 1) : '';
380
+ cookies.push({
381
+ name: name,
382
+ value: value,
383
+ domain: window.location.hostname,
384
+ path: '/',
385
+ httpOnly: false,
386
+ secure: window.location.protocol === 'https:',
387
+ sameSite: 'lax',
388
+ });
389
+ });
390
+ }
391
+
392
+ // Export localStorage
393
+ var localStorageData = {};
394
+ try {
395
+ for (var i = 0; i < localStorage.length; i++) {
396
+ var lsKey = localStorage.key(i);
397
+ if (lsKey !== null) localStorageData[lsKey] = localStorage.getItem(lsKey);
398
+ }
399
+ } catch (e) {
400
+ // localStorage may be unavailable (e.g. file:// or cross-origin)
401
+ }
402
+
403
+ // Export sessionStorage
404
+ var sessionStorageData = {};
405
+ try {
406
+ for (var j = 0; j < sessionStorage.length; j++) {
407
+ var ssKey = sessionStorage.key(j);
408
+ if (ssKey !== null) sessionStorageData[ssKey] = sessionStorage.getItem(ssKey);
409
+ }
410
+ } catch (e) {
411
+ // sessionStorage may be unavailable
412
+ }
413
+
414
+ return {
415
+ state: {
416
+ url: window.location.href,
417
+ cookies: cookies,
418
+ localStorage: localStorageData,
419
+ sessionStorage: sessionStorageData,
420
+ exportedAt: new Date().toISOString(),
421
+ },
422
+ };
423
+ `;
424
+ const result = await this.engine.executeJsInTab(tabUrl, js);
425
+ if (!result.ok)
426
+ throw new Error(result.error?.message ?? 'Storage state export failed');
427
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
428
+ }
429
+ async handleStorageStateImport(params) {
430
+ const start = Date.now();
431
+ const tabUrl = params['tabUrl'];
432
+ const state = params['state'];
433
+ // Serialise the state to inject into JS safely
434
+ const stateJson = JSON.stringify(state).replace(/\\/g, '\\\\').replace(/`/g, '\\`');
435
+ const js = `
436
+ var state = JSON.parse(\`${stateJson}\`);
437
+ var imported = { cookies: 0, localStorage: 0, sessionStorage: 0, errors: [] };
438
+
439
+ // Import cookies
440
+ if (state.cookies && Array.isArray(state.cookies)) {
441
+ state.cookies.forEach(function(cookie) {
442
+ try {
443
+ var parts = [cookie.name + '=' + cookie.value];
444
+ if (cookie.domain) parts.push('domain=' + cookie.domain);
445
+ parts.push('path=' + (cookie.path || '/'));
446
+ if (cookie.secure) parts.push('Secure');
447
+ if (cookie.sameSite) parts.push('SameSite=' + cookie.sameSite.charAt(0).toUpperCase() + cookie.sameSite.slice(1));
448
+ document.cookie = parts.join('; ');
449
+ imported.cookies++;
450
+ } catch (e) {
451
+ imported.errors.push('cookie:' + cookie.name + ':' + e.message);
452
+ }
453
+ });
454
+ }
455
+
456
+ // Import localStorage
457
+ if (state.localStorage && typeof state.localStorage === 'object') {
458
+ try {
459
+ Object.keys(state.localStorage).forEach(function(key) {
460
+ localStorage.setItem(key, state.localStorage[key]);
461
+ imported.localStorage++;
462
+ });
463
+ } catch (e) {
464
+ imported.errors.push('localStorage:' + e.message);
465
+ }
466
+ }
467
+
468
+ // Import sessionStorage
469
+ if (state.sessionStorage && typeof state.sessionStorage === 'object') {
470
+ try {
471
+ Object.keys(state.sessionStorage).forEach(function(key) {
472
+ sessionStorage.setItem(key, state.sessionStorage[key]);
473
+ imported.sessionStorage++;
474
+ });
475
+ } catch (e) {
476
+ imported.errors.push('sessionStorage:' + e.message);
477
+ }
478
+ }
479
+
480
+ return { imported: imported, success: imported.errors.length === 0 };
481
+ `;
482
+ const result = await this.engine.executeJsInTab(tabUrl, js);
483
+ if (!result.ok)
484
+ throw new Error(result.error?.message ?? 'Storage state import failed');
485
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
486
+ }
487
+ async handleLocalStorageGet(params) {
488
+ const start = Date.now();
489
+ const tabUrl = params['tabUrl'];
490
+ const key = params['key'];
491
+ const escapedKey = key.replace(/'/g, "\\'");
492
+ const js = `
493
+ var key = '${escapedKey}';
494
+ var value = null;
495
+ var exists = false;
496
+ try {
497
+ value = localStorage.getItem(key);
498
+ exists = value !== null;
499
+ } catch (e) {
500
+ throw new Error('localStorage not available: ' + e.message);
501
+ }
502
+ return { key: key, value: value, exists: exists };
503
+ `;
504
+ const result = await this.engine.executeJsInTab(tabUrl, js);
505
+ if (!result.ok)
506
+ throw new Error(result.error?.message ?? 'localStorage get failed');
507
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
508
+ }
509
+ async handleLocalStorageSet(params) {
510
+ const start = Date.now();
511
+ const tabUrl = params['tabUrl'];
512
+ const key = params['key'];
513
+ const value = params['value'];
514
+ const escapedKey = key.replace(/'/g, "\\'");
515
+ const escapedValue = value.replace(/'/g, "\\'");
516
+ const js = `
517
+ var key = '${escapedKey}';
518
+ var value = '${escapedValue}';
519
+ try {
520
+ localStorage.setItem(key, value);
521
+ } catch (e) {
522
+ throw new Error('localStorage set failed: ' + e.message);
523
+ }
524
+ return { key: key, value: value, set: true };
525
+ `;
526
+ const result = await this.engine.executeJsInTab(tabUrl, js);
527
+ if (!result.ok)
528
+ throw new Error(result.error?.message ?? 'localStorage set failed');
529
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
530
+ }
531
+ async handleSessionStorageGet(params) {
532
+ const start = Date.now();
533
+ const tabUrl = params['tabUrl'];
534
+ const key = params['key'];
535
+ const escapedKey = key.replace(/'/g, "\\'");
536
+ const js = `
537
+ var key = '${escapedKey}';
538
+ var value = null;
539
+ var exists = false;
540
+ try {
541
+ value = sessionStorage.getItem(key);
542
+ exists = value !== null;
543
+ } catch (e) {
544
+ throw new Error('sessionStorage not available: ' + e.message);
545
+ }
546
+ return { key: key, value: value, exists: exists };
547
+ `;
548
+ const result = await this.engine.executeJsInTab(tabUrl, js);
549
+ if (!result.ok)
550
+ throw new Error(result.error?.message ?? 'sessionStorage get failed');
551
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
552
+ }
553
+ async handleSessionStorageSet(params) {
554
+ const start = Date.now();
555
+ const tabUrl = params['tabUrl'];
556
+ const key = params['key'];
557
+ const value = params['value'];
558
+ const escapedKey = key.replace(/'/g, "\\'");
559
+ const escapedValue = value.replace(/'/g, "\\'");
560
+ const js = `
561
+ var key = '${escapedKey}';
562
+ var value = '${escapedValue}';
563
+ try {
564
+ sessionStorage.setItem(key, value);
565
+ } catch (e) {
566
+ throw new Error('sessionStorage set failed: ' + e.message);
567
+ }
568
+ return { key: key, value: value, set: true };
569
+ `;
570
+ const result = await this.engine.executeJsInTab(tabUrl, js);
571
+ if (!result.ok)
572
+ throw new Error(result.error?.message ?? 'sessionStorage set failed');
573
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
574
+ }
575
+ async handleIdbList(params) {
576
+ const start = Date.now();
577
+ const tabUrl = params['tabUrl'];
578
+ const js = `
579
+ return new Promise(function(resolve, reject) {
580
+ if (!indexedDB.databases) {
581
+ // Fallback: indexedDB.databases() not available in all environments
582
+ resolve({ databases: [], count: 0, note: 'indexedDB.databases() not supported in this context' });
583
+ return;
584
+ }
585
+ indexedDB.databases().then(function(dbs) {
586
+ resolve({ databases: dbs.map(function(db) { return { name: db.name, version: db.version }; }), count: dbs.length });
587
+ }).catch(function(err) {
588
+ reject(err);
589
+ });
590
+ });
591
+ `;
592
+ const result = await this.engine.executeJsInTab(tabUrl, js);
593
+ if (!result.ok)
594
+ throw new Error(result.error?.message ?? 'IndexedDB list failed');
595
+ return this.makeResponse(result.value ? JSON.parse(result.value) : { databases: [], count: 0 }, Date.now() - start);
596
+ }
597
+ async handleIdbGet(params) {
598
+ const start = Date.now();
599
+ const tabUrl = params['tabUrl'];
600
+ const database = params['database'];
601
+ const store = params['store'];
602
+ const query = params['query'];
603
+ const limit = typeof params['limit'] === 'number' ? params['limit'] : 100;
604
+ const queryJson = query ? JSON.stringify(query).replace(/\\/g, '\\\\').replace(/`/g, '\\`') : 'null';
605
+ const js = `
606
+ var dbName = ${JSON.stringify(database)};
607
+ var storeName = ${JSON.stringify(store)};
608
+ var queryParam = ${queryJson !== 'null' ? `JSON.parse(\`${queryJson}\`)` : 'null'};
609
+ var limit = ${limit};
610
+
611
+ return new Promise(function(resolve, reject) {
612
+ var req = indexedDB.open(dbName);
613
+ req.onerror = function() { reject(new Error('Failed to open database: ' + dbName)); };
614
+ req.onsuccess = function(event) {
615
+ var db = event.target.result;
616
+ if (!db.objectStoreNames.contains(storeName)) {
617
+ db.close();
618
+ reject(new Error('Object store not found: ' + storeName));
619
+ return;
620
+ }
621
+
622
+ var tx = db.transaction([storeName], 'readonly');
623
+ var objectStore = tx.objectStore(storeName);
624
+
625
+ // Build key range if query provided
626
+ var keyRange = null;
627
+ if (queryParam) {
628
+ try {
629
+ if (queryParam.only !== undefined) {
630
+ keyRange = IDBKeyRange.only(queryParam.only);
631
+ } else if (queryParam.lower !== undefined && queryParam.upper !== undefined) {
632
+ keyRange = IDBKeyRange.bound(queryParam.lower, queryParam.upper, !!queryParam.lowerOpen, !!queryParam.upperOpen);
633
+ } else if (queryParam.lower !== undefined) {
634
+ keyRange = IDBKeyRange.lowerBound(queryParam.lower, !!queryParam.lowerOpen);
635
+ } else if (queryParam.upper !== undefined) {
636
+ keyRange = IDBKeyRange.upperBound(queryParam.upper, !!queryParam.upperOpen);
637
+ }
638
+ } catch(e) {
639
+ db.close();
640
+ reject(new Error('Invalid key range: ' + e.message));
641
+ return;
642
+ }
643
+ }
644
+
645
+ var records = [];
646
+ var cursorReq = keyRange ? objectStore.openCursor(keyRange) : objectStore.openCursor();
647
+
648
+ cursorReq.onerror = function() {
649
+ db.close();
650
+ reject(new Error('Cursor error on store: ' + storeName));
651
+ };
652
+
653
+ cursorReq.onsuccess = function(event) {
654
+ var cursor = event.target.result;
655
+ if (cursor && records.length < limit) {
656
+ records.push({ key: cursor.key, value: cursor.value });
657
+ cursor.continue();
658
+ } else {
659
+ db.close();
660
+ resolve({ records: records, count: records.length, database: dbName, store: storeName });
661
+ }
662
+ };
663
+ };
664
+ });
665
+ `;
666
+ const result = await this.engine.executeJsInTab(tabUrl, js);
667
+ if (!result.ok)
668
+ throw new Error(result.error?.message ?? 'IndexedDB get failed');
669
+ return this.makeResponse(result.value ? JSON.parse(result.value) : { records: [], count: 0 }, Date.now() - start);
670
+ }
671
+ // ── Private helpers ─────────────────────────────────────────────────────────
672
+ makeResponse(data, latencyMs = 0) {
673
+ return {
674
+ content: [{ type: 'text', text: JSON.stringify(data) }],
675
+ metadata: { engine: 'applescript', degraded: false, latencyMs },
676
+ };
677
+ }
678
+ }
679
+ //# sourceMappingURL=storage.js.map