@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,269 @@
1
+ :root {
2
+ color-scheme: light;
3
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
+ color: #182033;
5
+ background: #f6f8fb;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ width: 380px;
14
+ min-height: 520px;
15
+ margin: 0;
16
+ background: #f6f8fb;
17
+ }
18
+
19
+ .shell {
20
+ min-height: 520px;
21
+ padding: 14px;
22
+ }
23
+
24
+ .topbar {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ gap: 12px;
29
+ margin-bottom: 12px;
30
+ }
31
+
32
+ .top-actions {
33
+ display: flex;
34
+ gap: 6px;
35
+ }
36
+
37
+ h1 {
38
+ margin: 0;
39
+ font-size: 17px;
40
+ line-height: 1.2;
41
+ }
42
+
43
+ p {
44
+ margin: 0;
45
+ }
46
+
47
+ #statusText {
48
+ margin-top: 3px;
49
+ font-size: 12px;
50
+ color: #667189;
51
+ }
52
+
53
+ button,
54
+ input {
55
+ font: inherit;
56
+ }
57
+
58
+ button {
59
+ border: 1px solid #cbd5e1;
60
+ background: #fff;
61
+ color: #172033;
62
+ border-radius: 7px;
63
+ min-height: 34px;
64
+ padding: 0 11px;
65
+ cursor: pointer;
66
+ }
67
+
68
+ button:hover {
69
+ background: #edf4ff;
70
+ }
71
+
72
+ .primary {
73
+ width: 100%;
74
+ border-color: #2458a6;
75
+ background: #2458a6;
76
+ color: #fff;
77
+ }
78
+
79
+ .primary:hover {
80
+ background: #1d4b91;
81
+ }
82
+
83
+ .icon-button {
84
+ min-width: 74px;
85
+ }
86
+
87
+ .hidden {
88
+ display: none !important;
89
+ }
90
+
91
+ .locked {
92
+ display: grid;
93
+ gap: 12px;
94
+ padding: 18px;
95
+ border: 1px solid #d7deea;
96
+ border-radius: 8px;
97
+ background: #fff;
98
+ }
99
+
100
+ .search-row {
101
+ margin-bottom: 10px;
102
+ }
103
+
104
+ .search-row input,
105
+ .field input,
106
+ .field select,
107
+ .field textarea,
108
+ .generated input {
109
+ width: 100%;
110
+ min-height: 36px;
111
+ border: 1px solid #cbd5e1;
112
+ border-radius: 7px;
113
+ padding: 0 10px;
114
+ background: #fff;
115
+ color: #182033;
116
+ }
117
+
118
+ .field textarea {
119
+ min-height: 74px;
120
+ padding-top: 8px;
121
+ resize: vertical;
122
+ }
123
+
124
+ .tabs {
125
+ display: grid;
126
+ grid-template-columns: repeat(4, 1fr);
127
+ gap: 4px;
128
+ margin-bottom: 10px;
129
+ }
130
+
131
+ .tab {
132
+ min-height: 31px;
133
+ padding: 0 6px;
134
+ font-size: 12px;
135
+ }
136
+
137
+ .tab.active {
138
+ border-color: #2458a6;
139
+ background: #e6f0ff;
140
+ color: #143f78;
141
+ }
142
+
143
+ .view {
144
+ display: none;
145
+ }
146
+
147
+ .view.active {
148
+ display: block;
149
+ }
150
+
151
+ .items {
152
+ display: grid;
153
+ gap: 8px;
154
+ max-height: 390px;
155
+ overflow: auto;
156
+ }
157
+
158
+ .item {
159
+ border: 1px solid #d7deea;
160
+ border-radius: 8px;
161
+ background: #fff;
162
+ padding: 10px;
163
+ }
164
+
165
+ .item-head {
166
+ display: flex;
167
+ align-items: start;
168
+ justify-content: space-between;
169
+ gap: 10px;
170
+ }
171
+
172
+ .item-title {
173
+ font-weight: 700;
174
+ font-size: 13px;
175
+ line-height: 1.3;
176
+ }
177
+
178
+ .item-subtitle {
179
+ margin-top: 3px;
180
+ color: #667189;
181
+ font-size: 12px;
182
+ overflow: hidden;
183
+ text-overflow: ellipsis;
184
+ white-space: nowrap;
185
+ max-width: 220px;
186
+ }
187
+
188
+ .pill {
189
+ border-radius: 999px;
190
+ background: #e7edf6;
191
+ color: #344866;
192
+ padding: 3px 7px;
193
+ font-size: 11px;
194
+ }
195
+
196
+ .actions {
197
+ display: grid;
198
+ grid-template-columns: repeat(4, 1fr);
199
+ gap: 6px;
200
+ margin-top: 10px;
201
+ }
202
+
203
+ .actions button {
204
+ min-height: 30px;
205
+ font-size: 12px;
206
+ padding: 0 6px;
207
+ }
208
+
209
+ .field {
210
+ display: grid;
211
+ gap: 5px;
212
+ margin-bottom: 9px;
213
+ font-size: 12px;
214
+ color: #526079;
215
+ }
216
+
217
+ .check {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ gap: 7px;
221
+ margin: 0 12px 10px 0;
222
+ font-size: 12px;
223
+ color: #526079;
224
+ }
225
+
226
+ .generated {
227
+ display: grid;
228
+ grid-template-columns: 1fr 72px;
229
+ gap: 6px;
230
+ margin-bottom: 10px;
231
+ }
232
+
233
+ .report {
234
+ display: grid;
235
+ gap: 8px;
236
+ }
237
+
238
+ .metric,
239
+ .issue {
240
+ border: 1px solid #d7deea;
241
+ border-radius: 8px;
242
+ background: #fff;
243
+ padding: 10px;
244
+ }
245
+
246
+ .metric strong {
247
+ display: block;
248
+ font-size: 22px;
249
+ }
250
+
251
+ .metric span,
252
+ .issue p {
253
+ color: #667189;
254
+ font-size: 12px;
255
+ }
256
+
257
+ .empty,
258
+ .error {
259
+ border: 1px solid #d7deea;
260
+ border-radius: 8px;
261
+ background: #fff;
262
+ padding: 14px;
263
+ color: #667189;
264
+ }
265
+
266
+ .error {
267
+ border-color: #f2b8b5;
268
+ color: #9f2f2a;
269
+ }
@@ -0,0 +1,99 @@
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</title>
7
+ <link rel="stylesheet" href="popup.css" />
8
+ </head>
9
+ <body>
10
+ <main class="shell">
11
+ <header class="topbar">
12
+ <div>
13
+ <h1>Wand Passwords</h1>
14
+ <p id="statusText">Locked</p>
15
+ </div>
16
+ <div class="top-actions">
17
+ <button id="siteToggleButton" class="icon-button" type="button" title="Toggle this site">Site</button>
18
+ <button id="optionsButton" class="icon-button" type="button" title="Options">Settings</button>
19
+ </div>
20
+ </header>
21
+
22
+ <section id="lockedView" class="locked">
23
+ <p>Sign in to Wand to use autofill and save passwords.</p>
24
+ <button id="openOptionsButton" class="primary" type="button">Open Options</button>
25
+ </section>
26
+
27
+ <section id="mainView" class="hidden">
28
+ <div class="search-row">
29
+ <input id="searchInput" type="search" placeholder="Search logins, cards, identities" autocomplete="off" />
30
+ </div>
31
+
32
+ <div class="tabs" role="tablist">
33
+ <button class="tab active" data-view="suggestions" type="button">Suggestions</button>
34
+ <button class="tab" data-view="generator" type="button">Generator</button>
35
+ <button class="tab" data-view="new" type="button">New</button>
36
+ <button class="tab" data-view="security" type="button">Security</button>
37
+ </div>
38
+
39
+ <section id="suggestionsView" class="view active">
40
+ <div id="itemsList" class="items"></div>
41
+ </section>
42
+
43
+ <section id="generatorView" class="view">
44
+ <label class="field">
45
+ <span>Length</span>
46
+ <input id="passwordLength" type="number" min="8" max="80" value="24" />
47
+ </label>
48
+ <label class="check"><input id="passwordDigits" type="checkbox" checked /> Digits</label>
49
+ <label class="check"><input id="passwordSymbols" type="checkbox" checked /> Symbols</label>
50
+ <div class="generated">
51
+ <input id="generatedPassword" type="text" readonly />
52
+ <button id="copyGeneratedButton" type="button">Copy</button>
53
+ </div>
54
+ <button id="generateButton" class="primary" type="button">Generate</button>
55
+ </section>
56
+
57
+ <section id="newView" class="view">
58
+ <label class="field">
59
+ <span>Type</span>
60
+ <select id="newType">
61
+ <option value="login">Login</option>
62
+ <option value="credit_card">Credit Card</option>
63
+ <option value="identity">Identity</option>
64
+ <option value="secure_note">Secure Note</option>
65
+ <option value="passkey">Passkey Metadata</option>
66
+ </select>
67
+ </label>
68
+ <label class="field">
69
+ <span>Title</span>
70
+ <input id="newTitle" type="text" />
71
+ </label>
72
+ <label class="field new-login">
73
+ <span>Username</span>
74
+ <input id="newUsername" type="text" autocomplete="username" />
75
+ </label>
76
+ <label class="field new-login">
77
+ <span>Password</span>
78
+ <input id="newPassword" type="password" autocomplete="new-password" />
79
+ </label>
80
+ <label class="field new-login">
81
+ <span>TOTP secret</span>
82
+ <input id="newTotp" type="text" />
83
+ </label>
84
+ <div id="typeFields"></div>
85
+ <label class="field">
86
+ <span>Notes</span>
87
+ <textarea id="newNotes" rows="3"></textarea>
88
+ </label>
89
+ <button id="saveNewButton" class="primary" type="button">Save Item</button>
90
+ </section>
91
+
92
+ <section id="securityView" class="view">
93
+ <div id="securityReport" class="report"></div>
94
+ </section>
95
+ </section>
96
+ </main>
97
+ <script type="module" src="popup.js"></script>
98
+ </body>
99
+ </html>
@@ -0,0 +1,342 @@
1
+ import { generatePassword } from "../shared/password-tools.mjs";
2
+ import { pageTitleFallback } from "../shared/url-tools.mjs";
3
+
4
+ var state = {
5
+ tab: null,
6
+ items: [],
7
+ report: null
8
+ };
9
+
10
+ var els = {
11
+ statusText: document.getElementById("statusText"),
12
+ lockedView: document.getElementById("lockedView"),
13
+ mainView: document.getElementById("mainView"),
14
+ optionsButton: document.getElementById("optionsButton"),
15
+ siteToggleButton: document.getElementById("siteToggleButton"),
16
+ openOptionsButton: document.getElementById("openOptionsButton"),
17
+ searchInput: document.getElementById("searchInput"),
18
+ itemsList: document.getElementById("itemsList"),
19
+ passwordLength: document.getElementById("passwordLength"),
20
+ passwordDigits: document.getElementById("passwordDigits"),
21
+ passwordSymbols: document.getElementById("passwordSymbols"),
22
+ generatedPassword: document.getElementById("generatedPassword"),
23
+ copyGeneratedButton: document.getElementById("copyGeneratedButton"),
24
+ generateButton: document.getElementById("generateButton"),
25
+ newType: document.getElementById("newType"),
26
+ newTitle: document.getElementById("newTitle"),
27
+ newUsername: document.getElementById("newUsername"),
28
+ newPassword: document.getElementById("newPassword"),
29
+ newTotp: document.getElementById("newTotp"),
30
+ typeFields: document.getElementById("typeFields"),
31
+ newNotes: document.getElementById("newNotes"),
32
+ saveNewButton: document.getElementById("saveNewButton"),
33
+ securityReport: document.getElementById("securityReport")
34
+ };
35
+
36
+ init();
37
+
38
+ async function init() {
39
+ bindEvents();
40
+ var tabs = await chrome.tabs.query({ active: true, currentWindow: true });
41
+ state.tab = tabs[0] || null;
42
+ if (state.tab?.url) {
43
+ els.newTitle.value = pageTitleFallback(state.tab.url);
44
+ }
45
+ await loadStatus();
46
+ }
47
+
48
+ function bindEvents() {
49
+ els.optionsButton.addEventListener("click", openOptions);
50
+ els.siteToggleButton.addEventListener("click", toggleSite);
51
+ els.openOptionsButton.addEventListener("click", openOptions);
52
+ els.searchInput.addEventListener("input", debounce(loadItems, 180));
53
+ els.generateButton.addEventListener("click", generateIntoField);
54
+ els.copyGeneratedButton.addEventListener("click", function () {
55
+ copyText(els.generatedPassword.value);
56
+ });
57
+ els.saveNewButton.addEventListener("click", saveNewLogin);
58
+ els.newType.addEventListener("change", renderTypeFields);
59
+ document.querySelectorAll(".tab").forEach(function (button) {
60
+ button.addEventListener("click", function () {
61
+ activateView(button.dataset.view);
62
+ });
63
+ });
64
+ renderTypeFields();
65
+ }
66
+
67
+ async function loadStatus() {
68
+ var response = await send({ type: "status" });
69
+ if (!response.ok) {
70
+ els.statusText.textContent = "Locked";
71
+ els.lockedView.classList.remove("hidden");
72
+ els.mainView.classList.add("hidden");
73
+ return;
74
+ }
75
+ els.statusText.textContent = "Connected";
76
+ els.lockedView.classList.add("hidden");
77
+ els.mainView.classList.remove("hidden");
78
+ await updateSiteToggle();
79
+ generateIntoField();
80
+ await Promise.all([loadItems(), loadSecurityReport()]);
81
+ }
82
+
83
+ async function loadItems() {
84
+ var q = els.searchInput.value.trim();
85
+ var response = await send({
86
+ type: "search-items",
87
+ q,
88
+ url: q ? "" : state.tab?.url,
89
+ limit: 30
90
+ });
91
+ if (!response.ok) {
92
+ renderError(els.itemsList, response.error);
93
+ return;
94
+ }
95
+ state.items = response.items || [];
96
+ renderItems();
97
+ }
98
+
99
+ function renderItems() {
100
+ els.itemsList.innerHTML = "";
101
+ if (!state.items.length) {
102
+ renderEmpty(els.itemsList, "No matching items.");
103
+ return;
104
+ }
105
+ state.items.forEach(function (item) {
106
+ var node = document.createElement("article");
107
+ node.className = "item";
108
+ node.innerHTML = [
109
+ "<div class='item-head'>",
110
+ "<div><div class='item-title'></div><div class='item-subtitle'></div></div>",
111
+ "<span class='pill'></span>",
112
+ "</div>",
113
+ "<div class='actions'>",
114
+ "<button type='button' data-action='fill'>Fill</button>",
115
+ "<button type='button' data-action='open'>Open</button>",
116
+ "<button type='button' data-action='copy-user'>User</button>",
117
+ "<button type='button' data-action='copy-pass'>Password</button>",
118
+ "</div>"
119
+ ].join("");
120
+ node.querySelector(".item-title").textContent = item.title || "Untitled";
121
+ node.querySelector(".item-subtitle").textContent = subtitle(item);
122
+ node.querySelector(".pill").textContent = label(item.type);
123
+ node.querySelector("[data-action='fill']").addEventListener("click", function () {
124
+ send({ type: "fill-active-tab", item });
125
+ window.close();
126
+ });
127
+ node.querySelector("[data-action='open']").addEventListener("click", function () {
128
+ send({ type: "open-and-fill", item });
129
+ window.close();
130
+ });
131
+ node.querySelector("[data-action='copy-user']").addEventListener("click", function () {
132
+ copyText(item.username || "");
133
+ });
134
+ node.querySelector("[data-action='copy-pass']").addEventListener("click", function () {
135
+ copyText(item.password || "");
136
+ });
137
+ els.itemsList.appendChild(node);
138
+ });
139
+ }
140
+
141
+ function generateIntoField() {
142
+ var password = generatePassword({
143
+ length: Number(els.passwordLength.value),
144
+ digits: els.passwordDigits.checked,
145
+ symbols: els.passwordSymbols.checked
146
+ });
147
+ els.generatedPassword.value = password;
148
+ }
149
+
150
+ async function saveNewLogin() {
151
+ var type = els.newType.value;
152
+ var fields = {};
153
+ if (type === "login" && els.newTotp.value.trim()) fields.totpSecret = els.newTotp.value.trim();
154
+ readDynamicFields(fields);
155
+ var response = await send({
156
+ type: "create-item",
157
+ item: {
158
+ type,
159
+ title: els.newTitle.value.trim() || pageTitleFallback(state.tab?.url || ""),
160
+ username: type === "login" ? els.newUsername.value.trim() : fields.email || fields.cardholder || fields.name || "",
161
+ password: type === "login" ? els.newPassword.value : "",
162
+ urls: state.tab?.url ? [state.tab.url] : [],
163
+ notes: els.newNotes.value.trim(),
164
+ fields,
165
+ tags: ["browser-extension"]
166
+ }
167
+ });
168
+ if (!response.ok) {
169
+ renderError(els.itemsList, response.error);
170
+ return;
171
+ }
172
+ els.newUsername.value = "";
173
+ els.newPassword.value = "";
174
+ els.newTotp.value = "";
175
+ els.newNotes.value = "";
176
+ renderTypeFields();
177
+ activateView("suggestions");
178
+ await loadItems();
179
+ }
180
+
181
+ function renderTypeFields() {
182
+ var type = els.newType.value;
183
+ document.querySelectorAll(".new-login").forEach(function (node) {
184
+ node.classList.toggle("hidden", type !== "login");
185
+ });
186
+ var fields = [];
187
+ if (type === "credit_card") {
188
+ fields = [
189
+ ["cardholder", "Cardholder"],
190
+ ["number", "Card Number"],
191
+ ["expMonth", "Expiry Month"],
192
+ ["expYear", "Expiry Year"],
193
+ ["cvc", "CVC"],
194
+ ["billingAddress", "Billing Address"]
195
+ ];
196
+ } else if (type === "identity") {
197
+ fields = [
198
+ ["name", "Full Name"],
199
+ ["email", "Email"],
200
+ ["phone", "Phone"],
201
+ ["organization", "Organization"],
202
+ ["address", "Address"],
203
+ ["city", "City"],
204
+ ["state", "State"],
205
+ ["postalCode", "Postal Code"],
206
+ ["country", "Country"]
207
+ ];
208
+ } else if (type === "passkey") {
209
+ fields = [
210
+ ["relyingPartyId", "Relying Party ID"],
211
+ ["credentialId", "Credential ID"],
212
+ ["userHandle", "User Handle"]
213
+ ];
214
+ }
215
+ els.typeFields.innerHTML = "";
216
+ fields.forEach(function (field) {
217
+ var label = document.createElement("label");
218
+ label.className = "field";
219
+ label.innerHTML = "<span></span><input type='text' data-field=''>";
220
+ label.querySelector("span").textContent = field[1];
221
+ label.querySelector("input").dataset.field = field[0];
222
+ els.typeFields.appendChild(label);
223
+ });
224
+ }
225
+
226
+ function readDynamicFields(fields) {
227
+ els.typeFields.querySelectorAll("[data-field]").forEach(function (input) {
228
+ if (input.value.trim()) fields[input.dataset.field] = input.value.trim();
229
+ });
230
+ }
231
+
232
+ async function updateSiteToggle() {
233
+ if (!state.tab?.url) return;
234
+ var response = await send({ type: "site-settings", url: state.tab.url });
235
+ if (response.ok) {
236
+ els.siteToggleButton.textContent = response.disabled ? "Enable Site" : "Disable Site";
237
+ }
238
+ }
239
+
240
+ async function toggleSite() {
241
+ if (!state.tab?.url) return;
242
+ var response = await send({ type: "toggle-site-disabled", url: state.tab.url });
243
+ if (response.ok) {
244
+ els.siteToggleButton.textContent = response.disabled ? "Enable Site" : "Disable Site";
245
+ }
246
+ }
247
+
248
+ async function loadSecurityReport() {
249
+ var response = await send({ type: "security-report" });
250
+ if (!response.ok) {
251
+ renderError(els.securityReport, response.error);
252
+ return;
253
+ }
254
+ state.report = response.report;
255
+ renderSecurityReport();
256
+ }
257
+
258
+ function renderSecurityReport() {
259
+ var report = state.report;
260
+ if (!report) return;
261
+ els.securityReport.innerHTML = "";
262
+ [
263
+ ["Items", report.totalItems],
264
+ ["Weak", report.weakPasswords],
265
+ ["Reused", report.reusedPasswords],
266
+ ["Missing URLs", report.missingUrls]
267
+ ].forEach(function (metric) {
268
+ var node = document.createElement("div");
269
+ node.className = "metric";
270
+ node.innerHTML = "<strong></strong><span></span>";
271
+ node.querySelector("strong").textContent = String(metric[1]);
272
+ node.querySelector("span").textContent = metric[0];
273
+ els.securityReport.appendChild(node);
274
+ });
275
+ (report.issues || []).slice(0, 5).forEach(function (issue) {
276
+ var node = document.createElement("div");
277
+ node.className = "issue";
278
+ node.innerHTML = "<strong></strong><p></p>";
279
+ node.querySelector("strong").textContent = issue.title;
280
+ node.querySelector("p").textContent = issue.message;
281
+ els.securityReport.appendChild(node);
282
+ });
283
+ }
284
+
285
+ function activateView(view) {
286
+ document.querySelectorAll(".tab").forEach(function (button) {
287
+ button.classList.toggle("active", button.dataset.view === view);
288
+ });
289
+ document.querySelectorAll(".view").forEach(function (section) {
290
+ section.classList.toggle("active", section.id === view + "View");
291
+ });
292
+ if (view === "security") loadSecurityReport();
293
+ }
294
+
295
+ function subtitle(item) {
296
+ if (item.fields && item.fields.providerLogin) return "Continue with " + item.fields.providerLogin;
297
+ if (item.type === "login") return item.username || (item.urls || [])[0] || "";
298
+ if (item.type === "credit_card") return "Card ending " + String((item.fields || {}).number || "").slice(-4);
299
+ if (item.type === "identity") return (item.fields || {}).email || (item.fields || {}).phone || "";
300
+ return (item.urls || [])[0] || "";
301
+ }
302
+
303
+ function label(type) {
304
+ return type === "credit_card" ? "Card" : type === "identity" ? "Identity" : type === "passkey" ? "Passkey" : "Login";
305
+ }
306
+
307
+ function renderEmpty(root, message) {
308
+ root.innerHTML = "";
309
+ var node = document.createElement("div");
310
+ node.className = "empty";
311
+ node.textContent = message;
312
+ root.appendChild(node);
313
+ }
314
+
315
+ function renderError(root, message) {
316
+ root.innerHTML = "";
317
+ var node = document.createElement("div");
318
+ node.className = "error";
319
+ node.textContent = message || "Something went wrong.";
320
+ root.appendChild(node);
321
+ }
322
+
323
+ function copyText(value) {
324
+ if (!value) return;
325
+ navigator.clipboard.writeText(value);
326
+ }
327
+
328
+ function openOptions() {
329
+ chrome.runtime.openOptionsPage();
330
+ }
331
+
332
+ function send(message) {
333
+ return chrome.runtime.sendMessage(message);
334
+ }
335
+
336
+ function debounce(fn, ms) {
337
+ var timer = 0;
338
+ return function () {
339
+ clearTimeout(timer);
340
+ timer = setTimeout(fn, ms);
341
+ };
342
+ }