@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.
- package/README.md +2 -0
- package/browser-extension/manifest.json +42 -0
- package/browser-extension/shared/password-tools.mjs +62 -0
- package/browser-extension/shared/url-tools.mjs +59 -0
- package/browser-extension/shared/webauthn-tools.mjs +378 -0
- package/browser-extension/src/api.js +79 -0
- package/browser-extension/src/background.js +428 -0
- package/browser-extension/src/content-script.js +354 -0
- package/browser-extension/src/options.css +118 -0
- package/browser-extension/src/options.html +39 -0
- package/browser-extension/src/options.js +101 -0
- package/browser-extension/src/popup.css +269 -0
- package/browser-extension/src/popup.html +99 -0
- package/browser-extension/src/popup.js +342 -0
- package/dist/build-info.json +3 -3
- package/dist/password-manager.d.ts +82 -0
- package/dist/password-manager.js +349 -0
- package/dist/server.js +194 -20
- package/dist/storage.d.ts +11 -0
- package/dist/storage.js +175 -0
- package/package.json +4 -2
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { apiFetch, getSettings, loginToWand, saveSettings } from "./api.js";
|
|
2
|
+
import { generatePassword } from "../shared/password-tools.mjs";
|
|
3
|
+
import { DEFAULT_BASE_URL, pageTitleFallback } from "../shared/url-tools.mjs";
|
|
4
|
+
import {
|
|
5
|
+
createPasskeyCredential,
|
|
6
|
+
getPasskeyAssertion,
|
|
7
|
+
parseWebAuthnRequestJson,
|
|
8
|
+
passkeyMatchesRequest,
|
|
9
|
+
webAuthnDomException
|
|
10
|
+
} from "../shared/webauthn-tools.mjs";
|
|
11
|
+
|
|
12
|
+
const DISABLED_ORIGINS_KEY = "wand.disabledOrigins";
|
|
13
|
+
var passkeyProxyState = {
|
|
14
|
+
supported: !!chrome.webAuthenticationProxy,
|
|
15
|
+
attached: false,
|
|
16
|
+
enabled: true,
|
|
17
|
+
lastError: ""
|
|
18
|
+
};
|
|
19
|
+
var canceledWebAuthnRequests = new Set();
|
|
20
|
+
|
|
21
|
+
chrome.runtime.onInstalled.addListener((details) => {
|
|
22
|
+
chrome.contextMenus.removeAll(() => {
|
|
23
|
+
chrome.contextMenus.create({
|
|
24
|
+
id: "wand-generate-password",
|
|
25
|
+
title: "Generate Wand password",
|
|
26
|
+
contexts: ["editable"]
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (details.reason === "install") {
|
|
31
|
+
saveSettings({ baseUrl: DEFAULT_BASE_URL, passkeyProxyEnabled: true })
|
|
32
|
+
.then(() => chrome.runtime.openOptionsPage())
|
|
33
|
+
.catch(() => {});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
38
|
+
if (info.menuItemId !== "wand-generate-password" || !tab?.id) return;
|
|
39
|
+
const password = generatePassword({ length: 24 });
|
|
40
|
+
await chrome.tabs.sendMessage(tab.id, { type: "wand-fill-generated-password", password }).catch(() => {});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (chrome.webAuthenticationProxy) {
|
|
44
|
+
chrome.webAuthenticationProxy.onCreateRequest.addListener((requestInfo) => {
|
|
45
|
+
handlePasskeyCreateRequest(requestInfo);
|
|
46
|
+
});
|
|
47
|
+
chrome.webAuthenticationProxy.onGetRequest.addListener((requestInfo) => {
|
|
48
|
+
handlePasskeyGetRequest(requestInfo);
|
|
49
|
+
});
|
|
50
|
+
chrome.webAuthenticationProxy.onIsUvpaaRequest.addListener((requestInfo) => {
|
|
51
|
+
completeIsUvpaaRequest(requestInfo.requestId, passkeyProxyState.attached);
|
|
52
|
+
});
|
|
53
|
+
chrome.webAuthenticationProxy.onRequestCanceled.addListener((requestId) => {
|
|
54
|
+
canceledWebAuthnRequests.add(requestId);
|
|
55
|
+
});
|
|
56
|
+
chrome.webAuthenticationProxy.onRemoteSessionStateChange.addListener(() => {
|
|
57
|
+
ensurePasskeyProxyAttachment().catch(() => {});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ensurePasskeyProxyAttachment().catch(() => {});
|
|
62
|
+
|
|
63
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
64
|
+
handleMessage(message, sender)
|
|
65
|
+
.then((result) => sendResponse({ ok: true, ...result }))
|
|
66
|
+
.catch((error) => sendResponse({ ok: false, error: error.message || String(error) }));
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
async function handleMessage(message, sender) {
|
|
71
|
+
switch (message?.type) {
|
|
72
|
+
case "get-settings":
|
|
73
|
+
return { settings: await getSettings() };
|
|
74
|
+
case "save-settings":
|
|
75
|
+
await saveSettings(message.settings || {});
|
|
76
|
+
await ensurePasskeyProxyAttachment();
|
|
77
|
+
return {};
|
|
78
|
+
case "login":
|
|
79
|
+
return { login: await loginAndAttach(message.baseUrl, message.password) };
|
|
80
|
+
case "status": {
|
|
81
|
+
const status = await apiFetch("/api/browser-extension/status");
|
|
82
|
+
await ensurePasskeyProxyAttachment();
|
|
83
|
+
return { ...status, passkeyProxy: await getPasskeyProxyState() };
|
|
84
|
+
}
|
|
85
|
+
case "vaults":
|
|
86
|
+
return await apiFetch("/api/browser-extension/vaults");
|
|
87
|
+
case "items-for-url": {
|
|
88
|
+
const response = await apiFetch(`/api/browser-extension/items?url=${encodeURIComponent(message.url)}&limit=20`);
|
|
89
|
+
return { ...response, items: sanitizeItemsForUi(response.items || []) };
|
|
90
|
+
}
|
|
91
|
+
case "search-items":
|
|
92
|
+
return await searchItems(message);
|
|
93
|
+
case "create-item":
|
|
94
|
+
return await apiFetch("/api/browser-extension/items", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: JSON.stringify(message.item)
|
|
97
|
+
});
|
|
98
|
+
case "update-item":
|
|
99
|
+
return await apiFetch(`/api/browser-extension/items/${encodeURIComponent(message.id)}`, {
|
|
100
|
+
method: "PUT",
|
|
101
|
+
body: JSON.stringify(message.item)
|
|
102
|
+
});
|
|
103
|
+
case "touch-item":
|
|
104
|
+
return await apiFetch(`/api/browser-extension/items/${encodeURIComponent(message.id)}/use`, { method: "POST" });
|
|
105
|
+
case "open-and-fill":
|
|
106
|
+
return await openAndFill(message.item);
|
|
107
|
+
case "delete-item":
|
|
108
|
+
return await apiFetch(`/api/browser-extension/items/${encodeURIComponent(message.id)}`, { method: "DELETE" });
|
|
109
|
+
case "security-report":
|
|
110
|
+
return await apiFetch("/api/browser-extension/security-report");
|
|
111
|
+
case "generate-password":
|
|
112
|
+
return { password: generatePassword(message.options || {}) };
|
|
113
|
+
case "preview-totp":
|
|
114
|
+
return await apiFetch("/api/browser-extension/totp/preview", {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: JSON.stringify({ secret: message.secret })
|
|
117
|
+
});
|
|
118
|
+
case "capture-login":
|
|
119
|
+
return await captureLogin(message, sender);
|
|
120
|
+
case "capture-federated-login":
|
|
121
|
+
return await captureFederatedLogin(message, sender);
|
|
122
|
+
case "fill-active-tab":
|
|
123
|
+
return await fillActiveTab(message.item);
|
|
124
|
+
case "site-settings":
|
|
125
|
+
return await getSiteSettings(message.url || sender.tab?.url);
|
|
126
|
+
case "toggle-site-disabled":
|
|
127
|
+
return await toggleSiteDisabled(message.url || sender.tab?.url);
|
|
128
|
+
case "passkey-proxy-state":
|
|
129
|
+
return { passkeyProxy: await getPasskeyProxyState() };
|
|
130
|
+
default:
|
|
131
|
+
throw new Error("Unsupported Wand extension message.");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function loginAndAttach(baseUrl, password) {
|
|
136
|
+
const login = await loginToWand(baseUrl, password);
|
|
137
|
+
await ensurePasskeyProxyAttachment();
|
|
138
|
+
return login;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function searchItems(message) {
|
|
142
|
+
const params = new URLSearchParams();
|
|
143
|
+
if (message.q) params.set("q", message.q);
|
|
144
|
+
if (message.url) params.set("url", message.url);
|
|
145
|
+
if (message.type) params.set("type", message.type);
|
|
146
|
+
params.set("limit", String(message.limit || 50));
|
|
147
|
+
const response = await apiFetch(`/api/browser-extension/items?${params.toString()}`);
|
|
148
|
+
return { ...response, items: sanitizeItemsForUi(response.items || []) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sanitizeItemsForUi(items) {
|
|
152
|
+
return items.map((item) => {
|
|
153
|
+
if (item.type !== "passkey") return item;
|
|
154
|
+
const fields = { ...(item.fields || {}) };
|
|
155
|
+
delete fields.privateKeyJwk;
|
|
156
|
+
delete fields.publicKeyJwk;
|
|
157
|
+
delete fields.publicKeyRaw;
|
|
158
|
+
delete fields.publicKeyCose;
|
|
159
|
+
return { ...item, fields };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function captureLogin(message, sender) {
|
|
164
|
+
const url = sender.tab?.url || message.url;
|
|
165
|
+
const username = String(message.username || "").trim();
|
|
166
|
+
const password = String(message.password || "").trim();
|
|
167
|
+
if (!url || !password) return { skipped: true };
|
|
168
|
+
const existing = await apiFetch(`/api/browser-extension/items?url=${encodeURIComponent(url)}&type=login&limit=20`);
|
|
169
|
+
const duplicate = (existing.items || []).find((item) => item.username === username);
|
|
170
|
+
if (duplicate) {
|
|
171
|
+
await apiFetch(`/api/browser-extension/items/${encodeURIComponent(duplicate.id)}`, {
|
|
172
|
+
method: "PUT",
|
|
173
|
+
body: JSON.stringify({ ...duplicate, password, urls: duplicate.urls?.length ? duplicate.urls : [url] })
|
|
174
|
+
});
|
|
175
|
+
return { item: duplicate, updated: true };
|
|
176
|
+
}
|
|
177
|
+
const item = {
|
|
178
|
+
type: "login",
|
|
179
|
+
title: message.title || pageTitleFallback(url),
|
|
180
|
+
username,
|
|
181
|
+
password,
|
|
182
|
+
urls: [url],
|
|
183
|
+
fields: {},
|
|
184
|
+
tags: ["browser-extension"]
|
|
185
|
+
};
|
|
186
|
+
return await apiFetch("/api/browser-extension/items", {
|
|
187
|
+
method: "POST",
|
|
188
|
+
body: JSON.stringify(item)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function captureFederatedLogin(message, sender) {
|
|
193
|
+
const url = sender.tab?.url || message.url;
|
|
194
|
+
const provider = String(message.provider || "").trim().toLowerCase();
|
|
195
|
+
if (!url || !provider) return { skipped: true };
|
|
196
|
+
const existing = await apiFetch(`/api/browser-extension/items?url=${encodeURIComponent(url)}&type=login&limit=50`);
|
|
197
|
+
const duplicate = (existing.items || []).find((item) => item.fields?.providerLogin === provider);
|
|
198
|
+
if (duplicate) return { item: duplicate, skipped: true };
|
|
199
|
+
const item = {
|
|
200
|
+
type: "login",
|
|
201
|
+
title: `${pageTitleFallback(url)} (${provider})`,
|
|
202
|
+
urls: [url],
|
|
203
|
+
fields: {
|
|
204
|
+
providerLogin: provider
|
|
205
|
+
},
|
|
206
|
+
tags: ["browser-extension", "federated-login"]
|
|
207
|
+
};
|
|
208
|
+
return await apiFetch("/api/browser-extension/items", {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: JSON.stringify(item)
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function fillActiveTab(item) {
|
|
215
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
216
|
+
if (!tab?.id) throw new Error("No active tab.");
|
|
217
|
+
await chrome.tabs.sendMessage(tab.id, { type: "wand-fill-item", item });
|
|
218
|
+
if (item?.id) {
|
|
219
|
+
await apiFetch(`/api/browser-extension/items/${encodeURIComponent(item.id)}/use`, { method: "POST" }).catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function openAndFill(item) {
|
|
225
|
+
const url = item?.urls?.[0];
|
|
226
|
+
if (!url) throw new Error("Item has no website URL.");
|
|
227
|
+
const tab = await chrome.tabs.create({ url });
|
|
228
|
+
await waitForTabLoaded(tab.id);
|
|
229
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
230
|
+
try {
|
|
231
|
+
await chrome.tabs.sendMessage(tab.id, { type: "wand-fill-item", item });
|
|
232
|
+
if (item.id) {
|
|
233
|
+
await apiFetch(`/api/browser-extension/items/${encodeURIComponent(item.id)}/use`, { method: "POST" }).catch(() => {});
|
|
234
|
+
}
|
|
235
|
+
return {};
|
|
236
|
+
} catch {
|
|
237
|
+
await delay(200);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw new Error("Opened the site, but the content script was not ready to fill.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function waitForTabLoaded(tabId) {
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
const timer = setTimeout(cleanup, 8000);
|
|
246
|
+
function cleanup() {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
249
|
+
resolve();
|
|
250
|
+
}
|
|
251
|
+
function listener(updatedTabId, info) {
|
|
252
|
+
if (updatedTabId === tabId && info.status === "complete") cleanup();
|
|
253
|
+
}
|
|
254
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
255
|
+
chrome.tabs.get(tabId).then((tab) => {
|
|
256
|
+
if (tab.status === "complete") cleanup();
|
|
257
|
+
}).catch(() => cleanup());
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function getSiteSettings(url) {
|
|
262
|
+
const origin = originForUrl(url);
|
|
263
|
+
if (!origin) return { disabled: false };
|
|
264
|
+
const data = await chrome.storage.local.get({ [DISABLED_ORIGINS_KEY]: {} });
|
|
265
|
+
return { disabled: data[DISABLED_ORIGINS_KEY]?.[origin] === true, origin };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function toggleSiteDisabled(url) {
|
|
269
|
+
const origin = originForUrl(url);
|
|
270
|
+
if (!origin) throw new Error("No active website.");
|
|
271
|
+
const data = await chrome.storage.local.get({ [DISABLED_ORIGINS_KEY]: {} });
|
|
272
|
+
const disabled = { ...(data[DISABLED_ORIGINS_KEY] || {}) };
|
|
273
|
+
disabled[origin] = disabled[origin] !== true;
|
|
274
|
+
await chrome.storage.local.set({ [DISABLED_ORIGINS_KEY]: disabled });
|
|
275
|
+
return { origin, disabled: disabled[origin] === true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function originForUrl(url) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = new URL(url);
|
|
281
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "";
|
|
282
|
+
return parsed.origin;
|
|
283
|
+
} catch {
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function delay(ms) {
|
|
289
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function ensurePasskeyProxyAttachment() {
|
|
293
|
+
const proxy = chrome.webAuthenticationProxy;
|
|
294
|
+
passkeyProxyState.supported = !!proxy;
|
|
295
|
+
if (!proxy) {
|
|
296
|
+
passkeyProxyState.attached = false;
|
|
297
|
+
passkeyProxyState.lastError = "chrome.webAuthenticationProxy is unavailable in this browser.";
|
|
298
|
+
return passkeyProxyState;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const settings = await getSettings();
|
|
302
|
+
passkeyProxyState.enabled = settings.passkeyProxyEnabled !== false;
|
|
303
|
+
if (!settings.appToken || !passkeyProxyState.enabled) {
|
|
304
|
+
if (passkeyProxyState.attached) {
|
|
305
|
+
await proxy.detach().catch(() => {});
|
|
306
|
+
}
|
|
307
|
+
passkeyProxyState.attached = false;
|
|
308
|
+
passkeyProxyState.lastError = "";
|
|
309
|
+
return passkeyProxyState;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const attachError = await proxy.attach();
|
|
314
|
+
passkeyProxyState.attached = !attachError;
|
|
315
|
+
passkeyProxyState.lastError = attachError || "";
|
|
316
|
+
} catch (error) {
|
|
317
|
+
passkeyProxyState.attached = false;
|
|
318
|
+
passkeyProxyState.lastError = error?.message || String(error);
|
|
319
|
+
}
|
|
320
|
+
return passkeyProxyState;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function getPasskeyProxyState() {
|
|
324
|
+
await ensurePasskeyProxyAttachment();
|
|
325
|
+
return { ...passkeyProxyState };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function handlePasskeyCreateRequest(requestInfo) {
|
|
329
|
+
try {
|
|
330
|
+
await ensurePasskeyProxyAttachment();
|
|
331
|
+
if (!passkeyProxyState.attached) {
|
|
332
|
+
await completeCreateError(requestInfo.requestId, "NotAllowedError", passkeyProxyState.lastError || "Wand passkey proxy is not attached.");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const options = parseWebAuthnRequestJson(requestInfo.requestDetailsJson);
|
|
336
|
+
const existing = await findPasskeysForRp(options.rp?.id || options.rpId);
|
|
337
|
+
const excludeCredentials = Array.isArray(options.excludeCredentials) ? options.excludeCredentials : [];
|
|
338
|
+
const excluded = existing.find((item) => {
|
|
339
|
+
const credentialId = item.fields?.credentialId;
|
|
340
|
+
return credentialId && excludeCredentials.some((credential) => credential?.id === credentialId);
|
|
341
|
+
});
|
|
342
|
+
if (excluded) {
|
|
343
|
+
await completeCreateError(requestInfo.requestId, "InvalidStateError", "A matching Wand passkey already exists.");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const created = await createPasskeyCredential(options);
|
|
347
|
+
if (canceledWebAuthnRequests.has(requestInfo.requestId)) return;
|
|
348
|
+
await apiFetch("/api/browser-extension/items", {
|
|
349
|
+
method: "POST",
|
|
350
|
+
body: JSON.stringify(created.item)
|
|
351
|
+
});
|
|
352
|
+
await chrome.webAuthenticationProxy.completeCreateRequest({
|
|
353
|
+
requestId: requestInfo.requestId,
|
|
354
|
+
responseJson: JSON.stringify(created.response)
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
await completeCreateError(requestInfo.requestId, "NotAllowedError", error.message || String(error));
|
|
358
|
+
} finally {
|
|
359
|
+
canceledWebAuthnRequests.delete(requestInfo.requestId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function handlePasskeyGetRequest(requestInfo) {
|
|
364
|
+
try {
|
|
365
|
+
await ensurePasskeyProxyAttachment();
|
|
366
|
+
if (!passkeyProxyState.attached) {
|
|
367
|
+
await completeGetError(requestInfo.requestId, "NotAllowedError", passkeyProxyState.lastError || "Wand passkey proxy is not attached.");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const options = parseWebAuthnRequestJson(requestInfo.requestDetailsJson);
|
|
371
|
+
const candidates = await findPasskeysForRp(options.rpId);
|
|
372
|
+
const item = candidates.find((candidate) => passkeyMatchesRequest(candidate, options));
|
|
373
|
+
if (!item) {
|
|
374
|
+
await completeGetError(requestInfo.requestId, "NotAllowedError", "No matching Wand passkey was found.");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const assertion = await getPasskeyAssertion(options, item);
|
|
378
|
+
if (canceledWebAuthnRequests.has(requestInfo.requestId)) return;
|
|
379
|
+
await apiFetch(`/api/browser-extension/items/${encodeURIComponent(item.id)}`, {
|
|
380
|
+
method: "PUT",
|
|
381
|
+
body: JSON.stringify({
|
|
382
|
+
...item,
|
|
383
|
+
fields: {
|
|
384
|
+
...item.fields,
|
|
385
|
+
signCount: String(assertion.signCount)
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
});
|
|
389
|
+
await chrome.webAuthenticationProxy.completeGetRequest({
|
|
390
|
+
requestId: requestInfo.requestId,
|
|
391
|
+
responseJson: JSON.stringify(assertion.response)
|
|
392
|
+
});
|
|
393
|
+
} catch (error) {
|
|
394
|
+
await completeGetError(requestInfo.requestId, "NotAllowedError", error.message || String(error));
|
|
395
|
+
} finally {
|
|
396
|
+
canceledWebAuthnRequests.delete(requestInfo.requestId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function findPasskeysForRp(rpId) {
|
|
401
|
+
const q = String(rpId || "").trim().toLowerCase();
|
|
402
|
+
if (!q) return [];
|
|
403
|
+
const response = await apiFetch(`/api/browser-extension/items?type=passkey&q=${encodeURIComponent(q)}&limit=100`);
|
|
404
|
+
return (response.items || []).filter((item) => item.fields?.rpId === q);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function completeCreateError(requestId, name, message) {
|
|
408
|
+
if (canceledWebAuthnRequests.has(requestId)) return;
|
|
409
|
+
await chrome.webAuthenticationProxy.completeCreateRequest({
|
|
410
|
+
requestId,
|
|
411
|
+
error: webAuthnDomException(name, message)
|
|
412
|
+
}).catch(() => {});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function completeGetError(requestId, name, message) {
|
|
416
|
+
if (canceledWebAuthnRequests.has(requestId)) return;
|
|
417
|
+
await chrome.webAuthenticationProxy.completeGetRequest({
|
|
418
|
+
requestId,
|
|
419
|
+
error: webAuthnDomException(name, message)
|
|
420
|
+
}).catch(() => {});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function completeIsUvpaaRequest(requestId, isUvpaa) {
|
|
424
|
+
await chrome.webAuthenticationProxy.completeIsUvpaaRequest({
|
|
425
|
+
requestId,
|
|
426
|
+
isUvpaa
|
|
427
|
+
}).catch(() => {});
|
|
428
|
+
}
|