latchkey 1.0.0 → 1.0.1
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/README.md +10 -6
- package/dist/package.json +1 -1
- package/dist/scripts/codegen/codeGenerator.d.ts +27 -0
- package/dist/scripts/codegen/codeGenerator.d.ts.map +1 -0
- package/dist/scripts/codegen/codeGenerator.js +220 -0
- package/dist/scripts/codegen/codeGenerator.js.map +1 -0
- package/dist/scripts/codegen/index.d.ts +27 -0
- package/dist/scripts/codegen/index.d.ts.map +1 -0
- package/dist/scripts/codegen/index.js +189 -0
- package/dist/scripts/codegen/index.js.map +1 -0
- package/dist/scripts/codegen/injectedScript.d.ts +6 -0
- package/dist/scripts/codegen/injectedScript.d.ts.map +1 -0
- package/dist/scripts/codegen/injectedScript.js +657 -0
- package/dist/scripts/codegen/injectedScript.js.map +1 -0
- package/dist/scripts/codegen/requestMetadataCollector.d.ts +15 -0
- package/dist/scripts/codegen/requestMetadataCollector.d.ts.map +1 -0
- package/dist/scripts/codegen/requestMetadataCollector.js +48 -0
- package/dist/scripts/codegen/requestMetadataCollector.js.map +1 -0
- package/dist/scripts/codegen/types.d.ts +77 -0
- package/dist/scripts/codegen/types.d.ts.map +1 -0
- package/dist/scripts/codegen/types.js +10 -0
- package/dist/scripts/codegen/types.js.map +1 -0
- package/dist/scripts/codegen.d.ts +24 -0
- package/dist/scripts/codegen.d.ts.map +1 -0
- package/dist/scripts/codegen.js +95 -0
- package/dist/scripts/codegen.js.map +1 -0
- package/dist/scripts/cryptFile.js +7 -2
- package/dist/scripts/cryptFile.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +6 -12
- package/dist/src/config.js.map +1 -1
- package/dist/src/encryptedStorage.d.ts +1 -2
- package/dist/src/encryptedStorage.d.ts.map +1 -1
- package/dist/src/encryptedStorage.js +18 -38
- package/dist/src/encryptedStorage.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/keychain.d.ts +0 -4
- package/dist/src/keychain.d.ts.map +1 -1
- package/dist/src/keychain.js +0 -13
- package/dist/src/keychain.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates the combined script injected into pages.
|
|
3
|
+
* Contains both the interaction recorder and the toolbar UI.
|
|
4
|
+
*/
|
|
5
|
+
export function createInjectedScript() {
|
|
6
|
+
return `
|
|
7
|
+
(function() {
|
|
8
|
+
// ===== SHARED HELPER FUNCTIONS =====
|
|
9
|
+
|
|
10
|
+
// Get the implicit ARIA role for an element
|
|
11
|
+
function getImplicitRole(element) {
|
|
12
|
+
const tag = element.tagName.toLowerCase();
|
|
13
|
+
const type = element.type ? element.type.toLowerCase() : '';
|
|
14
|
+
|
|
15
|
+
const roleMap = {
|
|
16
|
+
'a': element.href ? 'link' : null,
|
|
17
|
+
'article': 'article',
|
|
18
|
+
'aside': 'complementary',
|
|
19
|
+
'button': 'button',
|
|
20
|
+
'dialog': 'dialog',
|
|
21
|
+
'form': 'form',
|
|
22
|
+
'h1': 'heading',
|
|
23
|
+
'h2': 'heading',
|
|
24
|
+
'h3': 'heading',
|
|
25
|
+
'h4': 'heading',
|
|
26
|
+
'h5': 'heading',
|
|
27
|
+
'h6': 'heading',
|
|
28
|
+
'header': 'banner',
|
|
29
|
+
'footer': 'contentinfo',
|
|
30
|
+
'img': 'img',
|
|
31
|
+
'input': getInputRole(type),
|
|
32
|
+
'li': 'listitem',
|
|
33
|
+
'main': 'main',
|
|
34
|
+
'nav': 'navigation',
|
|
35
|
+
'ol': 'list',
|
|
36
|
+
'option': 'option',
|
|
37
|
+
'progress': 'progressbar',
|
|
38
|
+
'section': 'region',
|
|
39
|
+
'select': 'combobox',
|
|
40
|
+
'table': 'table',
|
|
41
|
+
'textarea': 'textbox',
|
|
42
|
+
'ul': 'list',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return roleMap[tag] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getInputRole(type) {
|
|
49
|
+
const inputRoles = {
|
|
50
|
+
'button': 'button',
|
|
51
|
+
'checkbox': 'checkbox',
|
|
52
|
+
'email': 'textbox',
|
|
53
|
+
'number': 'spinbutton',
|
|
54
|
+
'radio': 'radio',
|
|
55
|
+
'range': 'slider',
|
|
56
|
+
'search': 'searchbox',
|
|
57
|
+
'submit': 'button',
|
|
58
|
+
'tel': 'textbox',
|
|
59
|
+
'text': 'textbox',
|
|
60
|
+
'url': 'textbox',
|
|
61
|
+
};
|
|
62
|
+
return inputRoles[type] || 'textbox';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get accessible name for an element
|
|
66
|
+
function getAccessibleName(element) {
|
|
67
|
+
// Check aria-label first
|
|
68
|
+
if (element.getAttribute('aria-label')) {
|
|
69
|
+
return element.getAttribute('aria-label');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check aria-labelledby
|
|
73
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
74
|
+
if (labelledBy) {
|
|
75
|
+
const labelEl = document.getElementById(labelledBy);
|
|
76
|
+
if (labelEl) {
|
|
77
|
+
return labelEl.textContent.trim();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// For inputs, check associated label
|
|
82
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {
|
|
83
|
+
if (element.id) {
|
|
84
|
+
const label = document.querySelector('label[for="' + CSS.escape(element.id) + '"]');
|
|
85
|
+
if (label) {
|
|
86
|
+
return label.textContent.trim();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Check for wrapping label
|
|
90
|
+
const parentLabel = element.closest('label');
|
|
91
|
+
if (parentLabel) {
|
|
92
|
+
// Get text content excluding the input itself
|
|
93
|
+
const clone = parentLabel.cloneNode(true);
|
|
94
|
+
const inputs = clone.querySelectorAll('input, textarea, select');
|
|
95
|
+
inputs.forEach(function(input) { input.remove(); });
|
|
96
|
+
const text = clone.textContent.trim();
|
|
97
|
+
if (text) return text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For buttons and links, use text content
|
|
102
|
+
const tag = element.tagName.toLowerCase();
|
|
103
|
+
if (tag === 'button' || tag === 'a') {
|
|
104
|
+
const text = element.textContent.trim();
|
|
105
|
+
if (text && text.length < 100) {
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check title attribute
|
|
111
|
+
if (element.title) {
|
|
112
|
+
return element.title;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// For images, check alt
|
|
116
|
+
if (tag === 'img' && element.alt) {
|
|
117
|
+
return element.alt;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get element info for ancestry
|
|
124
|
+
function getElementInfo(element) {
|
|
125
|
+
const tag = element.tagName.toLowerCase();
|
|
126
|
+
const info = { tag: tag };
|
|
127
|
+
|
|
128
|
+
if (element.id) {
|
|
129
|
+
info.id = element.id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (element.className && typeof element.className === 'string' && element.className.trim()) {
|
|
133
|
+
info.className = element.className.trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (element.name) {
|
|
137
|
+
info.name = element.name;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Get role (explicit or implicit)
|
|
141
|
+
const explicitRole = element.getAttribute('role');
|
|
142
|
+
const role = explicitRole || getImplicitRole(element);
|
|
143
|
+
if (role) {
|
|
144
|
+
info.role = role;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Get accessible name
|
|
148
|
+
const accessibleName = getAccessibleName(element);
|
|
149
|
+
if (accessibleName) {
|
|
150
|
+
info.accessibleName = accessibleName;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// For inputs, capture type
|
|
154
|
+
if (tag === 'input' && element.type) {
|
|
155
|
+
info.inputType = element.type.toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Capture placeholder
|
|
159
|
+
if (element.placeholder) {
|
|
160
|
+
info.placeholder = element.placeholder;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return info;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Get full ancestry from element to body
|
|
167
|
+
function getAncestry(element) {
|
|
168
|
+
const ancestry = [];
|
|
169
|
+
let current = element;
|
|
170
|
+
|
|
171
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
172
|
+
ancestry.push(getElementInfo(current));
|
|
173
|
+
current = current.parentElement;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return ancestry;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if element is part of our toolbar
|
|
180
|
+
function isToolbarElement(element) {
|
|
181
|
+
return element.closest && element.closest('#latchkey-recorder-toolbar');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Global flag to track API key selection mode (set by toolbar, checked by recorder)
|
|
185
|
+
window.__latchkeyIsSelectingApiKey = false;
|
|
186
|
+
|
|
187
|
+
// ===== INTERACTION RECORDER =====
|
|
188
|
+
|
|
189
|
+
// Don't inject recorder twice
|
|
190
|
+
if (!window.__latchkeyRecorderInstalled) {
|
|
191
|
+
window.__latchkeyRecorderInstalled = true;
|
|
192
|
+
|
|
193
|
+
// Track clicks
|
|
194
|
+
document.addEventListener('click', (event) => {
|
|
195
|
+
const target = event.target;
|
|
196
|
+
// Don't record clicks when selecting API key element
|
|
197
|
+
if (!target || isToolbarElement(target) || window.__latchkeyIsSelectingApiKey) return;
|
|
198
|
+
|
|
199
|
+
const ancestry = getAncestry(target);
|
|
200
|
+
|
|
201
|
+
// Check if it's a checkbox or radio
|
|
202
|
+
if (target.tagName === 'INPUT') {
|
|
203
|
+
const inputType = target.type.toLowerCase();
|
|
204
|
+
if (inputType === 'checkbox') {
|
|
205
|
+
if (target.checked) {
|
|
206
|
+
window.__latchkeyRecordAction && window.__latchkeyRecordAction({
|
|
207
|
+
type: 'check',
|
|
208
|
+
ancestry: ancestry
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
window.__latchkeyRecordAction && window.__latchkeyRecordAction({
|
|
212
|
+
type: 'uncheck',
|
|
213
|
+
ancestry: ancestry
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
window.__latchkeyRecordAction && window.__latchkeyRecordAction({
|
|
221
|
+
type: 'click',
|
|
222
|
+
ancestry: ancestry
|
|
223
|
+
});
|
|
224
|
+
}, true);
|
|
225
|
+
|
|
226
|
+
// Track input/change for fill actions
|
|
227
|
+
let lastInputElement = null;
|
|
228
|
+
let lastInputValue = '';
|
|
229
|
+
let lastInputAncestry = [];
|
|
230
|
+
let inputTimeout = null;
|
|
231
|
+
|
|
232
|
+
document.addEventListener('input', (event) => {
|
|
233
|
+
const target = event.target;
|
|
234
|
+
if (!target || isToolbarElement(target)) return;
|
|
235
|
+
|
|
236
|
+
const tagName = target.tagName;
|
|
237
|
+
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || target.isContentEditable) {
|
|
238
|
+
const inputType = target.type ? target.type.toLowerCase() : 'text';
|
|
239
|
+
|
|
240
|
+
// Skip checkboxes and radios (handled by click)
|
|
241
|
+
if (inputType === 'checkbox' || inputType === 'radio') return;
|
|
242
|
+
|
|
243
|
+
lastInputElement = target;
|
|
244
|
+
lastInputValue = target.value || target.innerText || '';
|
|
245
|
+
lastInputAncestry = getAncestry(target);
|
|
246
|
+
|
|
247
|
+
// Debounce the recording to capture the final value
|
|
248
|
+
if (inputTimeout) clearTimeout(inputTimeout);
|
|
249
|
+
inputTimeout = setTimeout(() => {
|
|
250
|
+
if (lastInputElement) {
|
|
251
|
+
window.__latchkeyRecordAction && window.__latchkeyRecordAction({
|
|
252
|
+
type: 'fill',
|
|
253
|
+
ancestry: lastInputAncestry,
|
|
254
|
+
value: lastInputValue
|
|
255
|
+
});
|
|
256
|
+
lastInputElement = null;
|
|
257
|
+
lastInputValue = '';
|
|
258
|
+
lastInputAncestry = [];
|
|
259
|
+
}
|
|
260
|
+
}, 500);
|
|
261
|
+
}
|
|
262
|
+
}, true);
|
|
263
|
+
|
|
264
|
+
// Track select changes
|
|
265
|
+
document.addEventListener('change', (event) => {
|
|
266
|
+
const target = event.target;
|
|
267
|
+
if (!target || isToolbarElement(target)) return;
|
|
268
|
+
|
|
269
|
+
if (target.tagName === 'SELECT') {
|
|
270
|
+
const ancestry = getAncestry(target);
|
|
271
|
+
const selectedValue = target.value;
|
|
272
|
+
window.__latchkeyRecordAction && window.__latchkeyRecordAction({
|
|
273
|
+
type: 'select',
|
|
274
|
+
ancestry: ancestry,
|
|
275
|
+
value: selectedValue
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}, true);
|
|
279
|
+
|
|
280
|
+
// Track key presses (for special keys like Enter, Tab, etc.)
|
|
281
|
+
document.addEventListener('keydown', (event) => {
|
|
282
|
+
const target = event.target;
|
|
283
|
+
if (!target || isToolbarElement(target)) return;
|
|
284
|
+
|
|
285
|
+
// Only record special keys
|
|
286
|
+
const specialKeys = ['Enter', 'Tab', 'Escape', 'Backspace', 'Delete', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
|
287
|
+
if (specialKeys.includes(event.key)) {
|
|
288
|
+
const ancestry = getAncestry(target);
|
|
289
|
+
window.__latchkeyRecordAction && window.__latchkeyRecordAction({
|
|
290
|
+
type: 'press',
|
|
291
|
+
ancestry: ancestry,
|
|
292
|
+
key: event.key
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}, true);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ===== TOOLBAR UI =====
|
|
299
|
+
|
|
300
|
+
function createAndInjectToolbar() {
|
|
301
|
+
// Don't inject toolbar twice
|
|
302
|
+
if (document.getElementById('latchkey-recorder-toolbar')) return;
|
|
303
|
+
|
|
304
|
+
// State
|
|
305
|
+
let isSelectingApiKeyElement = false;
|
|
306
|
+
let highlightOverlay = null;
|
|
307
|
+
|
|
308
|
+
// Create styles
|
|
309
|
+
const style = document.createElement('style');
|
|
310
|
+
style.textContent = \`
|
|
311
|
+
#latchkey-recorder-toolbar {
|
|
312
|
+
position: fixed;
|
|
313
|
+
top: 0;
|
|
314
|
+
left: 50%;
|
|
315
|
+
transform: translateX(-50%);
|
|
316
|
+
z-index: 2147483647;
|
|
317
|
+
background: #1a1a1a;
|
|
318
|
+
border-radius: 0 0 8px 8px;
|
|
319
|
+
padding: 8px 16px;
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 8px;
|
|
323
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
324
|
+
font-size: 13px;
|
|
325
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
326
|
+
user-select: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#latchkey-recorder-toolbar * {
|
|
330
|
+
box-sizing: border-box;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.latchkey-toolbar-button {
|
|
334
|
+
background: #333;
|
|
335
|
+
border: 1px solid #555;
|
|
336
|
+
border-radius: 4px;
|
|
337
|
+
color: #fff;
|
|
338
|
+
padding: 6px 12px;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
font-size: 12px;
|
|
341
|
+
font-weight: 500;
|
|
342
|
+
transition: background 0.15s, border-color 0.15s;
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 6px;
|
|
346
|
+
white-space: nowrap;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.latchkey-toolbar-button:hover {
|
|
350
|
+
background: #444;
|
|
351
|
+
border-color: #666;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.latchkey-toolbar-button:active {
|
|
355
|
+
background: #555;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.latchkey-toolbar-button.active {
|
|
359
|
+
background: #2563eb;
|
|
360
|
+
border-color: #3b82f6;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.latchkey-toolbar-button.active:hover {
|
|
364
|
+
background: #1d4ed8;
|
|
365
|
+
border-color: #2563eb;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.latchkey-toolbar-button:disabled {
|
|
369
|
+
opacity: 0.5;
|
|
370
|
+
cursor: not-allowed;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.latchkey-toolbar-separator {
|
|
374
|
+
width: 1px;
|
|
375
|
+
height: 24px;
|
|
376
|
+
background: #444;
|
|
377
|
+
margin: 0 4px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.latchkey-toolbar-status {
|
|
381
|
+
color: #aaa;
|
|
382
|
+
font-size: 11px;
|
|
383
|
+
margin-left: 8px;
|
|
384
|
+
min-width: 140px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.latchkey-toolbar-status.pre-login {
|
|
388
|
+
color: #ff6b6b;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.latchkey-toolbar-status.post-login {
|
|
392
|
+
color: #4ade80;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.latchkey-phase-dot {
|
|
396
|
+
width: 8px;
|
|
397
|
+
height: 8px;
|
|
398
|
+
border-radius: 50%;
|
|
399
|
+
flex-shrink: 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.latchkey-phase-dot.pre-login {
|
|
403
|
+
background: #ff4444;
|
|
404
|
+
animation: latchkey-pulse 1.5s infinite;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.latchkey-phase-dot.post-login {
|
|
408
|
+
background: #4ade80;
|
|
409
|
+
animation: none;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@keyframes latchkey-pulse {
|
|
413
|
+
0%, 100% { opacity: 1; }
|
|
414
|
+
50% { opacity: 0.5; }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.latchkey-toolbar-gripper {
|
|
418
|
+
color: #666;
|
|
419
|
+
cursor: move;
|
|
420
|
+
padding: 4px;
|
|
421
|
+
display: flex;
|
|
422
|
+
align-items: center;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.latchkey-toolbar-gripper svg {
|
|
426
|
+
width: 16px;
|
|
427
|
+
height: 16px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#latchkey-highlight-overlay {
|
|
431
|
+
position: fixed;
|
|
432
|
+
pointer-events: none;
|
|
433
|
+
border: 2px solid #22c55e;
|
|
434
|
+
background: rgba(34, 197, 94, 0.1);
|
|
435
|
+
z-index: 2147483646;
|
|
436
|
+
transition: all 0.05s ease-out;
|
|
437
|
+
}
|
|
438
|
+
\`;
|
|
439
|
+
|
|
440
|
+
// Add styles to head (or documentElement if head doesn't exist)
|
|
441
|
+
(document.head || document.documentElement).appendChild(style);
|
|
442
|
+
|
|
443
|
+
// Create highlight overlay for element picker
|
|
444
|
+
highlightOverlay = document.createElement('div');
|
|
445
|
+
highlightOverlay.id = 'latchkey-highlight-overlay';
|
|
446
|
+
highlightOverlay.style.display = 'none';
|
|
447
|
+
document.body.appendChild(highlightOverlay);
|
|
448
|
+
|
|
449
|
+
// Create toolbar
|
|
450
|
+
const toolbar = document.createElement('div');
|
|
451
|
+
toolbar.id = 'latchkey-recorder-toolbar';
|
|
452
|
+
|
|
453
|
+
// Gripper for dragging
|
|
454
|
+
const gripper = document.createElement('div');
|
|
455
|
+
gripper.className = 'latchkey-toolbar-gripper';
|
|
456
|
+
gripper.innerHTML = \`
|
|
457
|
+
<svg viewBox="0 0 16 16" fill="currentColor">
|
|
458
|
+
<path d="M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"/>
|
|
459
|
+
</svg>
|
|
460
|
+
\`;
|
|
461
|
+
toolbar.appendChild(gripper);
|
|
462
|
+
|
|
463
|
+
// Phase indicator dot
|
|
464
|
+
const phaseDot = document.createElement('div');
|
|
465
|
+
phaseDot.className = 'latchkey-phase-dot pre-login';
|
|
466
|
+
toolbar.appendChild(phaseDot);
|
|
467
|
+
|
|
468
|
+
// Status text
|
|
469
|
+
const status = document.createElement('span');
|
|
470
|
+
status.className = 'latchkey-toolbar-status pre-login';
|
|
471
|
+
status.textContent = 'Recording (pre-login)';
|
|
472
|
+
toolbar.appendChild(status);
|
|
473
|
+
|
|
474
|
+
// Separator
|
|
475
|
+
const sep1 = document.createElement('div');
|
|
476
|
+
sep1.className = 'latchkey-toolbar-separator';
|
|
477
|
+
toolbar.appendChild(sep1);
|
|
478
|
+
|
|
479
|
+
// "I've logged in" button - transitions from pre-login to post-login
|
|
480
|
+
const loggedInBtn = document.createElement('button');
|
|
481
|
+
loggedInBtn.className = 'latchkey-toolbar-button';
|
|
482
|
+
loggedInBtn.innerHTML = \`
|
|
483
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
484
|
+
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
|
485
|
+
</svg>
|
|
486
|
+
I've logged in
|
|
487
|
+
\`;
|
|
488
|
+
loggedInBtn.title = 'Click after you have successfully logged in to start recording';
|
|
489
|
+
toolbar.appendChild(loggedInBtn);
|
|
490
|
+
|
|
491
|
+
// Separator
|
|
492
|
+
const sep2 = document.createElement('div');
|
|
493
|
+
sep2.className = 'latchkey-toolbar-separator';
|
|
494
|
+
toolbar.appendChild(sep2);
|
|
495
|
+
|
|
496
|
+
// "Select API Key Element" button
|
|
497
|
+
const apiKeyBtn = document.createElement('button');
|
|
498
|
+
apiKeyBtn.className = 'latchkey-toolbar-button';
|
|
499
|
+
apiKeyBtn.innerHTML = \`
|
|
500
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
501
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"/>
|
|
502
|
+
</svg>
|
|
503
|
+
Select API Key Element
|
|
504
|
+
\`;
|
|
505
|
+
apiKeyBtn.title = 'Click to select the element containing the API key';
|
|
506
|
+
toolbar.appendChild(apiKeyBtn);
|
|
507
|
+
|
|
508
|
+
// Append toolbar to body
|
|
509
|
+
document.body.appendChild(toolbar);
|
|
510
|
+
|
|
511
|
+
// Function to update UI based on current phase
|
|
512
|
+
function updatePhaseUI(phase) {
|
|
513
|
+
// Update dot
|
|
514
|
+
phaseDot.className = 'latchkey-phase-dot ' + phase;
|
|
515
|
+
|
|
516
|
+
// Update status text
|
|
517
|
+
status.className = 'latchkey-toolbar-status ' + phase;
|
|
518
|
+
switch (phase) {
|
|
519
|
+
case 'pre-login':
|
|
520
|
+
status.textContent = 'Not recording (log in first)';
|
|
521
|
+
loggedInBtn.disabled = false;
|
|
522
|
+
break;
|
|
523
|
+
case 'post-login':
|
|
524
|
+
status.textContent = 'Recording';
|
|
525
|
+
loggedInBtn.disabled = true;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Button handler
|
|
531
|
+
loggedInBtn.onclick = () => {
|
|
532
|
+
if (loggedInBtn.disabled) return;
|
|
533
|
+
window.__latchkeyTransitionToPostLogin && window.__latchkeyTransitionToPostLogin();
|
|
534
|
+
updatePhaseUI('post-login');
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
apiKeyBtn.onclick = () => {
|
|
538
|
+
isSelectingApiKeyElement = !isSelectingApiKeyElement;
|
|
539
|
+
window.__latchkeyIsSelectingApiKey = isSelectingApiKeyElement;
|
|
540
|
+
apiKeyBtn.classList.toggle('active', isSelectingApiKeyElement);
|
|
541
|
+
highlightOverlay.style.display = isSelectingApiKeyElement ? 'block' : 'none';
|
|
542
|
+
if (isSelectingApiKeyElement) {
|
|
543
|
+
status.textContent = 'Click the API key element...';
|
|
544
|
+
} else {
|
|
545
|
+
// Restore status based on current phase (async)
|
|
546
|
+
if (window.__latchkeyGetPhase) {
|
|
547
|
+
window.__latchkeyGetPhase().then(function(phase) {
|
|
548
|
+
updatePhaseUI(phase || 'pre-login');
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// Element picker: highlight on mousemove
|
|
555
|
+
document.addEventListener('mousemove', (e) => {
|
|
556
|
+
if (!isSelectingApiKeyElement) return;
|
|
557
|
+
|
|
558
|
+
const target = e.target;
|
|
559
|
+
if (!target || target === highlightOverlay || toolbar.contains(target)) {
|
|
560
|
+
highlightOverlay.style.display = 'none';
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const rect = target.getBoundingClientRect();
|
|
565
|
+
highlightOverlay.style.display = 'block';
|
|
566
|
+
highlightOverlay.style.left = rect.left + 'px';
|
|
567
|
+
highlightOverlay.style.top = rect.top + 'px';
|
|
568
|
+
highlightOverlay.style.width = rect.width + 'px';
|
|
569
|
+
highlightOverlay.style.height = rect.height + 'px';
|
|
570
|
+
}, true);
|
|
571
|
+
|
|
572
|
+
// Element picker: select on click
|
|
573
|
+
document.addEventListener('click', (e) => {
|
|
574
|
+
// Handle API key element selection
|
|
575
|
+
if (isSelectingApiKeyElement) {
|
|
576
|
+
const target = e.target;
|
|
577
|
+
if (!target || target === highlightOverlay || toolbar.contains(target)) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
e.stopPropagation();
|
|
583
|
+
|
|
584
|
+
isSelectingApiKeyElement = false;
|
|
585
|
+
window.__latchkeyIsSelectingApiKey = false;
|
|
586
|
+
apiKeyBtn.classList.remove('active');
|
|
587
|
+
highlightOverlay.style.display = 'none';
|
|
588
|
+
|
|
589
|
+
// Capture ancestry for the element
|
|
590
|
+
const ancestry = getAncestry(target);
|
|
591
|
+
window.__latchkeyApiKeyElementSelected && window.__latchkeyApiKeyElementSelected(ancestry);
|
|
592
|
+
|
|
593
|
+
// Restore status based on current phase (async)
|
|
594
|
+
if (window.__latchkeyGetPhase) {
|
|
595
|
+
window.__latchkeyGetPhase().then(function(phase) {
|
|
596
|
+
updatePhaseUI(phase || 'pre-login');
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}, true);
|
|
601
|
+
|
|
602
|
+
// Make toolbar draggable
|
|
603
|
+
let isDragging = false;
|
|
604
|
+
let offsetX = 0;
|
|
605
|
+
let offsetY = 0;
|
|
606
|
+
|
|
607
|
+
gripper.addEventListener('mousedown', (e) => {
|
|
608
|
+
isDragging = true;
|
|
609
|
+
const rect = toolbar.getBoundingClientRect();
|
|
610
|
+
offsetX = e.clientX - rect.left;
|
|
611
|
+
offsetY = e.clientY - rect.top;
|
|
612
|
+
toolbar.style.transition = 'none';
|
|
613
|
+
e.preventDefault();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
document.addEventListener('mousemove', (e) => {
|
|
617
|
+
if (!isDragging) return;
|
|
618
|
+
const x = e.clientX - offsetX;
|
|
619
|
+
const y = e.clientY - offsetY;
|
|
620
|
+
toolbar.style.left = x + 'px';
|
|
621
|
+
toolbar.style.top = y + 'px';
|
|
622
|
+
toolbar.style.transform = 'none';
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
document.addEventListener('mouseup', () => {
|
|
626
|
+
isDragging = false;
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Expose function to update UI when phase changes externally
|
|
630
|
+
window.__latchkeyUpdatePhase = updatePhaseUI;
|
|
631
|
+
|
|
632
|
+
// Initialize UI based on current phase (async because exposeFunction returns Promise)
|
|
633
|
+
if (window.__latchkeyGetPhase) {
|
|
634
|
+
window.__latchkeyGetPhase().then(function(phase) {
|
|
635
|
+
updatePhaseUI(phase || 'pre-login');
|
|
636
|
+
}).catch(function() {
|
|
637
|
+
updatePhaseUI('pre-login');
|
|
638
|
+
});
|
|
639
|
+
} else {
|
|
640
|
+
updatePhaseUI('pre-login');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Wait for DOM to be ready before creating toolbar
|
|
645
|
+
if (document.body) {
|
|
646
|
+
createAndInjectToolbar();
|
|
647
|
+
} else if (document.readyState === 'loading') {
|
|
648
|
+
document.addEventListener('DOMContentLoaded', createAndInjectToolbar);
|
|
649
|
+
} else {
|
|
650
|
+
// readyState is 'interactive' or 'complete' but body doesn't exist yet
|
|
651
|
+
// Use a short timeout to wait for body
|
|
652
|
+
setTimeout(createAndInjectToolbar, 0);
|
|
653
|
+
}
|
|
654
|
+
})();
|
|
655
|
+
`;
|
|
656
|
+
}
|
|
657
|
+
//# sourceMappingURL=injectedScript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injectedScript.js","sourceRoot":"","sources":["../../../scripts/codegen/injectedScript.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyoBR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects and manages HTTP request metadata.
|
|
3
|
+
*/
|
|
4
|
+
import type { Request, Response } from 'playwright';
|
|
5
|
+
import type { RecordingPhase } from './types.js';
|
|
6
|
+
export declare class RequestMetadataCollector {
|
|
7
|
+
private readonly requests;
|
|
8
|
+
private readonly outputPath;
|
|
9
|
+
private currentPhase;
|
|
10
|
+
constructor(outputPath: string);
|
|
11
|
+
setPhase(phase: RecordingPhase): void;
|
|
12
|
+
addRequest(request: Request, response: Response | null): void;
|
|
13
|
+
flush(): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=requestMetadataCollector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestMetadataCollector.d.ts","sourceRoot":"","sources":["../../../scripts/codegen/requestMetadataCollector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,YAAY,CAAC;AAElE,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,YAAY,CAA+B;gBAEvC,UAAU,EAAE,MAAM;IAI9B,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAIrC,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI;IAmC7D,KAAK,IAAI,IAAI;CAGd"}
|