@co0ontty/wand 2.1.1 → 2.2.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.
@@ -0,0 +1,354 @@
1
+ (function () {
2
+ if (window.__wandPasswordsLoaded) return;
3
+ window.__wandPasswordsLoaded = true;
4
+
5
+ var state = {
6
+ items: [],
7
+ activeInput: null,
8
+ root: null,
9
+ panel: null,
10
+ savePromptOpen: false,
11
+ disabled: false
12
+ };
13
+
14
+ loadSiteSettings();
15
+ refreshItems();
16
+ document.addEventListener("focusin", onFocusIn, true);
17
+ document.addEventListener("input", onInput, true);
18
+ document.addEventListener("click", onClick, true);
19
+ document.addEventListener("submit", onSubmit, true);
20
+ chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
21
+ if (message.type === "wand-fill-item") {
22
+ fillItem(message.item);
23
+ sendResponse({ ok: true });
24
+ return;
25
+ }
26
+ if (message.type === "wand-fill-generated-password") {
27
+ fillGeneratedPassword(message.password);
28
+ sendResponse({ ok: true });
29
+ }
30
+ });
31
+
32
+ function refreshItems() {
33
+ if (state.disabled) return;
34
+ chrome.runtime.sendMessage({ type: "items-for-url", url: location.href }, function (response) {
35
+ if (response && response.ok) {
36
+ state.items = response.items || [];
37
+ }
38
+ });
39
+ }
40
+
41
+ function onFocusIn(event) {
42
+ var input = event.target;
43
+ if (state.disabled) return;
44
+ if (!isFillableInput(input)) return;
45
+ state.activeInput = input;
46
+ renderPanel(input);
47
+ }
48
+
49
+ function onInput(event) {
50
+ var input = event.target;
51
+ if (state.disabled) return;
52
+ if (input === state.activeInput && isFillableInput(input)) {
53
+ renderPanel(input);
54
+ }
55
+ }
56
+
57
+ function onSubmit(event) {
58
+ var form = event.target;
59
+ if (state.disabled) return;
60
+ if (!form || state.savePromptOpen) return;
61
+ var login = extractLoginFromForm(form);
62
+ if (!login || !login.password) return;
63
+ state.savePromptOpen = true;
64
+ var shouldSave = window.confirm("Save this login to Wand Passwords?");
65
+ state.savePromptOpen = false;
66
+ if (!shouldSave) return;
67
+ chrome.runtime.sendMessage({
68
+ type: "capture-login",
69
+ title: document.title || location.hostname,
70
+ url: location.href,
71
+ username: login.username,
72
+ password: login.password
73
+ }, function () {
74
+ refreshItems();
75
+ });
76
+ }
77
+
78
+ function onClick(event) {
79
+ if (state.disabled) return;
80
+ var provider = detectFederatedProvider(event.target);
81
+ if (!provider) return;
82
+ var alreadySaved = state.items.some(function (item) {
83
+ return item.fields && item.fields.providerLogin === provider;
84
+ });
85
+ if (alreadySaved) return;
86
+ var shouldSave = window.confirm("Remember this sign-in choice in Wand Passwords?");
87
+ if (!shouldSave) return;
88
+ chrome.runtime.sendMessage({
89
+ type: "capture-federated-login",
90
+ title: document.title || location.hostname,
91
+ url: location.href,
92
+ provider: provider
93
+ });
94
+ }
95
+
96
+ function loadSiteSettings() {
97
+ chrome.runtime.sendMessage({ type: "site-settings", url: location.href }, function (response) {
98
+ state.disabled = !!(response && response.ok && response.disabled);
99
+ if (state.disabled) hidePanel();
100
+ });
101
+ }
102
+
103
+ function renderPanel(input) {
104
+ var candidates = matchingItems(input);
105
+ if (!candidates.length) {
106
+ hidePanel();
107
+ return;
108
+ }
109
+ ensurePanel();
110
+ var rect = input.getBoundingClientRect();
111
+ state.root.style.left = Math.max(8, Math.min(window.innerWidth - 330, rect.left + window.scrollX)) + "px";
112
+ state.root.style.top = (rect.bottom + window.scrollY + 6) + "px";
113
+ state.panel.innerHTML = "";
114
+ candidates.slice(0, 6).forEach(function (item) {
115
+ var button = document.createElement("button");
116
+ button.className = "wand-fill-row";
117
+ button.type = "button";
118
+ button.innerHTML = "<span><strong></strong><small></small></span><em></em>";
119
+ button.querySelector("strong").textContent = item.title || "Untitled";
120
+ button.querySelector("small").textContent = subtitleForItem(item);
121
+ button.querySelector("em").textContent = labelForType(item.type);
122
+ button.addEventListener("mousedown", function (event) {
123
+ event.preventDefault();
124
+ fillItem(item);
125
+ chrome.runtime.sendMessage({ type: "touch-item", id: item.id });
126
+ hidePanel();
127
+ });
128
+ state.panel.appendChild(button);
129
+ });
130
+ }
131
+
132
+ function ensurePanel() {
133
+ if (state.root && state.panel) return;
134
+ state.root = document.createElement("div");
135
+ state.root.className = "wand-fill-root";
136
+ var shadow = state.root.attachShadow({ mode: "open" });
137
+ var style = document.createElement("style");
138
+ style.textContent = [
139
+ ":host{all:initial;position:absolute;z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}",
140
+ ".panel{width:320px;max-height:300px;overflow:auto;padding:6px;border:1px solid #c9d3e2;border-radius:8px;background:#fff;box-shadow:0 14px 40px rgba(14,30,54,.18);color:#172033}",
141
+ ".wand-fill-row{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;border:0;background:transparent;text-align:left;border-radius:6px;padding:9px 10px;color:#172033;cursor:pointer}",
142
+ ".wand-fill-row:hover,.wand-fill-row:focus{background:#edf4ff;outline:none}",
143
+ "strong{display:block;font-size:13px;font-weight:700;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
144
+ "small{display:block;margin-top:3px;font-size:12px;color:#5d6b82;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
145
+ "em{font-style:normal;font-size:11px;color:#31507a;background:#e4efff;border-radius:999px;padding:3px 7px;white-space:nowrap}"
146
+ ].join("");
147
+ state.panel = document.createElement("div");
148
+ state.panel.className = "panel";
149
+ shadow.append(style, state.panel);
150
+ document.documentElement.appendChild(state.root);
151
+ document.addEventListener("pointerdown", function (event) {
152
+ if (!state.root.contains(event.target) && event.target !== state.activeInput) hidePanel();
153
+ }, true);
154
+ }
155
+
156
+ function hidePanel() {
157
+ if (state.root) {
158
+ state.root.remove();
159
+ state.root = null;
160
+ state.panel = null;
161
+ }
162
+ }
163
+
164
+ function matchingItems(input) {
165
+ var kind = classifyInput(input);
166
+ return state.items.filter(function (item) {
167
+ if (kind === "login") return item.type === "login";
168
+ if (kind === "card") return item.type === "credit_card";
169
+ if (kind === "identity") return item.type === "identity";
170
+ return item.type === "login" || item.type === "identity";
171
+ });
172
+ }
173
+
174
+ function fillItem(item) {
175
+ if (!item) return;
176
+ if (item.type === "login") fillLogin(item);
177
+ if (item.type === "credit_card") fillCreditCard(item);
178
+ if (item.type === "identity") fillIdentity(item);
179
+ }
180
+
181
+ function fillLogin(item) {
182
+ if (item.fields && item.fields.providerLogin) {
183
+ if (clickProviderButton(item.fields.providerLogin)) return;
184
+ }
185
+ var form = findFormForActiveInput();
186
+ var username = findUsernameInput(form) || findInputByAutocomplete("username");
187
+ var password = findPasswordInput(form);
188
+ var otp = findOtpInput(form);
189
+ if (username && item.username) setNativeValue(username, item.username);
190
+ if (password && item.password) setNativeValue(password, item.password);
191
+ if (otp && item.fields && item.fields.totpSecret) {
192
+ chrome.runtime.sendMessage({ type: "preview-totp", secret: item.fields.totpSecret }, function (response) {
193
+ if (response && response.ok && response.code) setNativeValue(otp, response.code);
194
+ });
195
+ }
196
+ }
197
+
198
+ function fillCreditCard(item) {
199
+ var fields = item.fields || {};
200
+ setFirst(["cc-name", "name"], fields.cardholder || fields.name);
201
+ setFirst(["cc-number", "cardnumber", "card-number"], fields.number);
202
+ setFirst(["cc-exp-month", "exp-month"], fields.expMonth);
203
+ setFirst(["cc-exp-year", "exp-year"], fields.expYear);
204
+ setFirst(["cc-exp", "expiration"], [fields.expMonth, fields.expYear].filter(Boolean).join("/"));
205
+ setFirst(["cc-csc", "cvc", "cvv"], fields.cvc);
206
+ }
207
+
208
+ function fillIdentity(item) {
209
+ var fields = item.fields || {};
210
+ setFirst(["name", "fullname"], fields.name || item.username);
211
+ setFirst(["email"], fields.email);
212
+ setFirst(["tel", "phone"], fields.phone);
213
+ setFirst(["organization"], fields.organization);
214
+ setFirst(["street-address", "address-line1"], fields.address);
215
+ setFirst(["address-level2", "city"], fields.city);
216
+ setFirst(["address-level1", "state"], fields.state);
217
+ setFirst(["postal-code", "zip"], fields.postalCode);
218
+ setFirst(["country"], fields.country);
219
+ }
220
+
221
+ function fillGeneratedPassword(password) {
222
+ var target = state.activeInput || document.activeElement;
223
+ if (isFillableInput(target)) setNativeValue(target, password);
224
+ }
225
+
226
+ function setFirst(tokens, value) {
227
+ if (!value) return;
228
+ var input = findInputByTokens(tokens);
229
+ if (input) setNativeValue(input, value);
230
+ }
231
+
232
+ function findFormForActiveInput() {
233
+ return state.activeInput && state.activeInput.form ? state.activeInput.form : null;
234
+ }
235
+
236
+ function extractLoginFromForm(form) {
237
+ var password = findPasswordInput(form);
238
+ if (!password || !password.value) return null;
239
+ var username = findUsernameInput(form);
240
+ return {
241
+ username: username ? username.value : "",
242
+ password: password.value
243
+ };
244
+ }
245
+
246
+ function findPasswordInput(form) {
247
+ return firstInput(form, "input[type='password']");
248
+ }
249
+
250
+ function findUsernameInput(form) {
251
+ return firstInput(form, "input[autocomplete='username'],input[type='email'],input[type='text'],input:not([type])");
252
+ }
253
+
254
+ function findOtpInput(form) {
255
+ return firstInput(form, "input[autocomplete='one-time-code'],input[name*='otp' i],input[id*='otp' i],input[name*='code' i],input[id*='code' i]");
256
+ }
257
+
258
+ function firstInput(form, selector) {
259
+ var root = form || document;
260
+ return root.querySelector(selector);
261
+ }
262
+
263
+ function findInputByAutocomplete(value) {
264
+ return document.querySelector("input[autocomplete='" + value + "']");
265
+ }
266
+
267
+ function findInputByTokens(tokens) {
268
+ var inputs = Array.prototype.slice.call(document.querySelectorAll("input, textarea, select"));
269
+ return inputs.find(function (input) {
270
+ var text = [
271
+ input.autocomplete,
272
+ input.name,
273
+ input.id,
274
+ input.getAttribute("aria-label"),
275
+ input.placeholder
276
+ ].join(" ").toLowerCase();
277
+ return tokens.some(function (token) { return text.indexOf(token.toLowerCase()) >= 0; });
278
+ });
279
+ }
280
+
281
+ function setNativeValue(input, value) {
282
+ if (!input) return;
283
+ input.focus();
284
+ input.value = value;
285
+ input.dispatchEvent(new Event("input", { bubbles: true }));
286
+ input.dispatchEvent(new Event("change", { bubbles: true }));
287
+ }
288
+
289
+ function clickProviderButton(provider) {
290
+ var providerName = String(provider || "").toLowerCase();
291
+ var candidates = Array.prototype.slice.call(document.querySelectorAll("button,a,[role='button'],input[type='button'],input[type='submit']"));
292
+ var hit = candidates.find(function (node) {
293
+ return detectFederatedProvider(node) === providerName;
294
+ });
295
+ if (!hit) return false;
296
+ hit.click();
297
+ return true;
298
+ }
299
+
300
+ function detectFederatedProvider(target) {
301
+ var node = target && target.closest ? target.closest("button,a,[role='button'],input[type='button'],input[type='submit']") : null;
302
+ if (!node) return "";
303
+ var text = [
304
+ node.textContent,
305
+ node.getAttribute("aria-label"),
306
+ node.getAttribute("title"),
307
+ node.value,
308
+ node.href
309
+ ].join(" ").toLowerCase();
310
+ if (!/(sign|log|continue|connect|with|oauth|sso|github|google|apple|microsoft|facebook|gitlab|twitter|x\\.com)/.test(text)) return "";
311
+ if (/google|gmail/.test(text)) return "google";
312
+ if (/apple/.test(text)) return "apple";
313
+ if (/microsoft|live\\.com|office|azure/.test(text)) return "microsoft";
314
+ if (/github/.test(text)) return "github";
315
+ if (/gitlab/.test(text)) return "gitlab";
316
+ if (/facebook/.test(text)) return "facebook";
317
+ if (/twitter|x\\.com/.test(text)) return "twitter";
318
+ return "";
319
+ }
320
+
321
+ function isFillableInput(input) {
322
+ if (!input || !input.matches) return false;
323
+ if (!input.matches("input, textarea")) return false;
324
+ var type = (input.getAttribute("type") || "text").toLowerCase();
325
+ return !["button", "submit", "reset", "checkbox", "radio", "file", "hidden"].includes(type);
326
+ }
327
+
328
+ function classifyInput(input) {
329
+ var text = [
330
+ input.autocomplete,
331
+ input.name,
332
+ input.id,
333
+ input.getAttribute("aria-label"),
334
+ input.placeholder,
335
+ input.type
336
+ ].join(" ").toLowerCase();
337
+ if (/password|username|email|login|one-time-code|otp|code/.test(text)) return "login";
338
+ if (/cc-|card|cvc|cvv|expir/.test(text)) return "card";
339
+ if (/address|name|phone|tel|postal|zip|city|country|organization/.test(text)) return "identity";
340
+ return "other";
341
+ }
342
+
343
+ function subtitleForItem(item) {
344
+ if (item.fields && item.fields.providerLogin) return "Continue with " + item.fields.providerLogin;
345
+ if (item.type === "login") return item.username || (item.urls || [])[0] || "";
346
+ if (item.type === "credit_card") return "Card ending " + String((item.fields || {}).number || "").slice(-4);
347
+ if (item.type === "identity") return (item.fields || {}).email || (item.fields || {}).phone || "";
348
+ return (item.urls || [])[0] || "";
349
+ }
350
+
351
+ function labelForType(type) {
352
+ return type === "credit_card" ? "Card" : type === "identity" ? "ID" : type === "passkey" ? "Passkey" : "Login";
353
+ }
354
+ })();
@@ -0,0 +1,118 @@
1
+ :root {
2
+ color-scheme: light;
3
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
+ color: #172033;
5
+ background: #f5f7fb;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ background: #f5f7fb;
15
+ }
16
+
17
+ .page {
18
+ min-height: 100vh;
19
+ display: grid;
20
+ place-items: start center;
21
+ padding: 48px 20px;
22
+ }
23
+
24
+ .panel {
25
+ width: min(560px, 100%);
26
+ border: 1px solid #d7deea;
27
+ border-radius: 8px;
28
+ background: #fff;
29
+ padding: 22px;
30
+ box-shadow: 0 16px 40px rgba(18, 35, 64, .08);
31
+ }
32
+
33
+ h1 {
34
+ margin: 0;
35
+ font-size: 22px;
36
+ }
37
+
38
+ p {
39
+ margin: 6px 0 0;
40
+ color: #667189;
41
+ }
42
+
43
+ .field {
44
+ display: grid;
45
+ gap: 7px;
46
+ margin-top: 16px;
47
+ font-size: 13px;
48
+ color: #526079;
49
+ }
50
+
51
+ .check {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ gap: 8px;
55
+ margin-top: 14px;
56
+ font-size: 13px;
57
+ color: #526079;
58
+ }
59
+
60
+ .check input {
61
+ width: 16px;
62
+ min-height: 16px;
63
+ }
64
+
65
+ input {
66
+ width: 100%;
67
+ min-height: 40px;
68
+ border: 1px solid #cbd5e1;
69
+ border-radius: 7px;
70
+ padding: 0 11px;
71
+ font: inherit;
72
+ color: #172033;
73
+ }
74
+
75
+ .actions {
76
+ display: flex;
77
+ gap: 8px;
78
+ margin-top: 18px;
79
+ }
80
+
81
+ button {
82
+ min-height: 38px;
83
+ border: 1px solid #cbd5e1;
84
+ border-radius: 7px;
85
+ background: #fff;
86
+ padding: 0 14px;
87
+ font: inherit;
88
+ color: #172033;
89
+ cursor: pointer;
90
+ }
91
+
92
+ button:hover {
93
+ background: #edf4ff;
94
+ }
95
+
96
+ .primary {
97
+ border-color: #2458a6;
98
+ background: #2458a6;
99
+ color: #fff;
100
+ }
101
+
102
+ .primary:hover {
103
+ background: #1d4b91;
104
+ }
105
+
106
+ .status {
107
+ min-height: 20px;
108
+ margin-top: 14px;
109
+ font-size: 13px;
110
+ }
111
+
112
+ .status.error {
113
+ color: #a33a32;
114
+ }
115
+
116
+ .status.ok {
117
+ color: #1f6b45;
118
+ }
@@ -0,0 +1,39 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Wand Passwords Options</title>
7
+ <link rel="stylesheet" href="options.css" />
8
+ </head>
9
+ <body>
10
+ <main class="page">
11
+ <section class="panel">
12
+ <header>
13
+ <h1>Wand Passwords</h1>
14
+ <p>Connect this browser extension to your Wand server.</p>
15
+ </header>
16
+ <label class="field">
17
+ <span>Server URL</span>
18
+ <input id="baseUrl" type="url" value="https://home.huniu.fun:8183" autocomplete="url" />
19
+ </label>
20
+ <label class="field">
21
+ <span>Wand password</span>
22
+ <input id="password" type="password" autocomplete="current-password" />
23
+ </label>
24
+ <label class="check">
25
+ <input id="passkeyProxyEnabled" type="checkbox" checked />
26
+ Enable passkey proxy
27
+ </label>
28
+ <div class="actions">
29
+ <button id="loginButton" class="primary" type="button">Sign In</button>
30
+ <button id="testButton" type="button">Test</button>
31
+ <button id="lockButton" type="button">Lock</button>
32
+ </div>
33
+ <p id="status" class="status"></p>
34
+ <p id="passkeyStatus" class="status"></p>
35
+ </section>
36
+ </main>
37
+ <script type="module" src="options.js"></script>
38
+ </body>
39
+ </html>
@@ -0,0 +1,101 @@
1
+ import { getSettings, saveSettings, apiFetch } from "./api.js";
2
+ import { DEFAULT_BASE_URL, normalizeBaseUrl } from "../shared/url-tools.mjs";
3
+
4
+ var els = {
5
+ baseUrl: document.getElementById("baseUrl"),
6
+ password: document.getElementById("password"),
7
+ passkeyProxyEnabled: document.getElementById("passkeyProxyEnabled"),
8
+ loginButton: document.getElementById("loginButton"),
9
+ testButton: document.getElementById("testButton"),
10
+ lockButton: document.getElementById("lockButton"),
11
+ status: document.getElementById("status"),
12
+ passkeyStatus: document.getElementById("passkeyStatus")
13
+ };
14
+
15
+ init();
16
+
17
+ async function init() {
18
+ var settings = await getSettings().catch(function () {
19
+ return { baseUrl: DEFAULT_BASE_URL, appToken: "" };
20
+ });
21
+ els.baseUrl.value = settings.baseUrl || DEFAULT_BASE_URL;
22
+ els.passkeyProxyEnabled.checked = settings.passkeyProxyEnabled !== false;
23
+ setStatus(settings.appToken ? "Connected." : "Locked.", settings.appToken ? "ok" : "");
24
+ await refreshPasskeyState();
25
+ els.loginButton.addEventListener("click", login);
26
+ els.testButton.addEventListener("click", testConnection);
27
+ els.lockButton.addEventListener("click", lock);
28
+ els.baseUrl.addEventListener("change", function () {
29
+ saveSettings({ baseUrl: normalizeBaseUrl(els.baseUrl.value) });
30
+ });
31
+ els.passkeyProxyEnabled.addEventListener("change", async function () {
32
+ await send({ type: "save-settings", settings: { passkeyProxyEnabled: els.passkeyProxyEnabled.checked } });
33
+ await refreshPasskeyState();
34
+ });
35
+ }
36
+
37
+ async function login() {
38
+ try {
39
+ setStatus("Signing in...");
40
+ var response = await send({ type: "login", baseUrl: els.baseUrl.value, password: els.password.value });
41
+ if (!response.ok) throw new Error(response.error || "Login failed.");
42
+ els.password.value = "";
43
+ setStatus("Connected.", "ok");
44
+ await refreshPasskeyState();
45
+ } catch (error) {
46
+ setStatus(error.message || "Login failed.", "error");
47
+ }
48
+ }
49
+
50
+ async function testConnection() {
51
+ try {
52
+ await saveSettings({ baseUrl: normalizeBaseUrl(els.baseUrl.value) });
53
+ var status = await apiFetch("/api/browser-extension/status");
54
+ setStatus(status.ok ? "Connection works." : "Connection failed.", status.ok ? "ok" : "error");
55
+ await refreshPasskeyState();
56
+ } catch (error) {
57
+ setStatus(error.message || "Connection failed.", "error");
58
+ }
59
+ }
60
+
61
+ async function lock() {
62
+ await saveSettings({ appToken: "" });
63
+ setStatus("Locked.");
64
+ await refreshPasskeyState();
65
+ }
66
+
67
+ function setStatus(message, kind) {
68
+ els.status.textContent = message;
69
+ els.status.className = "status";
70
+ if (kind) els.status.classList.add(kind);
71
+ }
72
+
73
+ async function refreshPasskeyState() {
74
+ var response = await send({ type: "passkey-proxy-state" }).catch(function (error) {
75
+ return { ok: false, error: error.message || String(error) };
76
+ });
77
+ if (!response.ok) {
78
+ setPasskeyStatus(response.error || "Passkey proxy unavailable.", "error");
79
+ return;
80
+ }
81
+ var proxy = response.passkeyProxy || {};
82
+ if (!proxy.supported) {
83
+ setPasskeyStatus("Passkey proxy is not supported by this browser.", "error");
84
+ } else if (!proxy.enabled) {
85
+ setPasskeyStatus("Passkey proxy is disabled.");
86
+ } else if (proxy.attached) {
87
+ setPasskeyStatus("Passkey proxy is active.", "ok");
88
+ } else {
89
+ setPasskeyStatus(proxy.lastError || "Passkey proxy is not attached.", "error");
90
+ }
91
+ }
92
+
93
+ function setPasskeyStatus(message, kind) {
94
+ els.passkeyStatus.textContent = message;
95
+ els.passkeyStatus.className = "status";
96
+ if (kind) els.passkeyStatus.classList.add(kind);
97
+ }
98
+
99
+ function send(message) {
100
+ return chrome.runtime.sendMessage(message);
101
+ }