032574cf5b9a

additionalUrls + remember-site toast + 30% glow reduction (v1.9.0)

time
2026-04-24 23:21 UTC · 6 months ago
machine
koh2
parent
17d54c31d9c5
14 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.ts+11 −0src/background/vault.test.ts+53 −0src/background/vault.ts+48 −5src/content/index.ts+12 −7src/content/inject.ts+9 −9src/popup/App.tsx+10 −4src/popup/styles/popup.css+54 −12src/popup/styles/vars.css+3 −3src/popup/views/Entry.tsx+67 −16src/popup/views/Vault.tsx+17 −4src/shared/types.ts+8 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "1.8.9",4+  "version": "1.9.0",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "1.8.9",4+  "version": "1.9.0",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "1.8.9",3+  "version": "1.9.0",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/messages.ts+11 −0
@@ -145,6 +145,17 @@145145         await vault.updateEntry(id, patch);146146         return { ok: true };147147       }148+      case "ADD_ADDITIONAL_URL": {149+        const { id, url } = msg.payload as { id: string; url: string };150+        const trimmed = typeof url === "string" ? url.trim() : "";151+        if (!trimmed) return { ok: false, error: "url required" };152+        const existing = vault.getEntryById(id);153+        if (!existing) return { ok: false, error: "entry not found" };154+        if (existing.type !== "login") return { ok: false, error: "login entries only" };155+        const merged = [...(existing.additionalUrls ?? []), trimmed];156+        await vault.updateEntry(id, { additionalUrls: merged });157+        return { ok: true };158+      }148159       case "DELETE_ENTRY": {149160         const { id } = msg.payload as { id: string };150161         await vault.deleteEntry(id);

view file

src/background/vault.test.ts+53 −0
@@ -828,4 +828,57 @@828828 829829     expect(await vault.isCardDeclined("1111", "06", "2028")).toBe(false);830830   });831+832+  it("queryDomain matches an entry against its additionalUrls", async () => {833+    const vault = await import("./vault");834+835+    await vault.createVault("multi-domain-pass-12345");836+    const entry = await vault.addEntry({837+      type: "login",838+      title: "Foo",839+      username: "june",840+      password: "secret",841+      url: "https://foo.com",842+      additionalUrls: ["https://bar.io", "https://baz.org"],843+    });844+845+    // Vault index keys by eTLD+1 (tldts.getDomain), so additionalUrls only846+    // add new buckets when the eTLD+1 is genuinely different from the primary.847+    expect(vault.queryDomain("foo.com").map((e) => e.id)).toContain(entry.id);848+    expect(vault.queryDomain("bar.io").map((e) => e.id)).toContain(entry.id);849+    expect(vault.queryDomain("baz.org").map((e) => e.id)).toContain(entry.id);850+    expect(vault.queryDomain("unrelated.com")).toEqual([]);851+  });852+853+  it("updateEntry trims, dedupes, and clears empty additionalUrls", async () => {854+    const vault = await import("./vault");855+856+    await vault.createVault("dedupe-additional-pass-12345");857+    const entry = await vault.addEntry({858+      type: "login",859+      title: "Foo",860+      username: "june",861+      password: "secret",862+      url: "https://foo.com",863+    });864+865+    await vault.updateEntry(entry.id, {866+      additionalUrls: [867+        "  https://login.foo.com  ",868+        "https://login.foo.com",869+        "",870+        "  ",871+        "https://accounts.foo.com",872+      ],873+    });874+875+    const stored = vault.getEntryById(entry.id);876+    expect(stored?.additionalUrls).toEqual([877+      "https://login.foo.com",878+      "https://accounts.foo.com",879+    ]);880+881+    await vault.updateEntry(entry.id, { additionalUrls: ["", "   "] });882+    expect(vault.getEntryById(entry.id)?.additionalUrls).toBeUndefined();883+  });831884 });

view file

src/background/vault.ts+48 −5
@@ -330,12 +330,24 @@330330     if (entry.deletedAt) continue;331331     if (entry.type === "note" || entry.type === "passkey" || entry.type === "card") continue;332332 333-    const domain = extractDomain(entry.url ?? "");334-    if (!domain) continue;333+    // An entry can match multiple sites via `additionalUrls`. Index each334+    // hostname so `queryDomain` returns the entry regardless of which alias335+    // the user is currently visiting. Dedupe per-entry so a single entry336+    // never appears twice in one bucket.337+    const domains = new Set<string>();338+    const primary = extractDomain(entry.url ?? "");339+    if (primary) domains.add(primary);340+    for (const extra of entry.additionalUrls ?? []) {341+      const d = extractDomain(extra);342+      if (d) domains.add(d);343+    }344+    if (domains.size === 0) continue;335345 336-    const bucket = domainIndex.get(domain) ?? [];337-    bucket.push(entry);338-    domainIndex.set(domain, bucket);346+    for (const domain of domains) {347+      const bucket = domainIndex.get(domain) ?? [];348+      bucket.push(entry);349+      domainIndex.set(domain, bucket);350+    }339351   }340352 }341353 @@ -877,6 +889,21 @@877889   buildDomainIndexSync(entries);878890 }879891 892+function cleanAdditionalUrls(values: string[] | undefined): string[] | undefined {893+  if (!Array.isArray(values)) return undefined;894+  const seen = new Set<string>();895+  const cleaned: string[] = [];896+  for (const value of values) {897+    const trimmed = typeof value === "string" ? value.trim() : "";898+    if (!trimmed) continue;899+    const key = trimmed.toLowerCase();900+    if (seen.has(key)) continue;901+    seen.add(key);902+    cleaned.push(trimmed);903+  }904+  return cleaned.length > 0 ? cleaned : undefined;905+}906+880907 function sanitizeEntryDraft(entry: EntryDraft): EntryDraft {881908   const title = entry.title.trim();882909   const type = entry.type ?? "login";@@ -912,6 +939,7 @@912939     username: type === "login" ? entry.username?.trim() ?? "" : undefined,913940     password: type === "login" ? entry.password : undefined,914941     url: type === "login" ? entry.url?.trim() ?? "" : undefined,942+    additionalUrls: type === "login" ? cleanAdditionalUrls(entry.additionalUrls) : undefined,915943     totp: type === "login" ? entry.totp?.trim() || undefined : undefined,916944     notes: type === "passkey" ? undefined : entry.notes?.trim() || undefined,917945     breach: type === "login" ? entry.breach : undefined,@@ -957,6 +985,21 @@957985 958986   if ("url" in patch) {959987     nextPatch.url = patch.url?.trim() ?? "";988+  }989+990+  if ("additionalUrls" in patch) {991+    const incoming = Array.isArray(patch.additionalUrls) ? patch.additionalUrls : [];992+    const seen = new Set<string>();993+    const cleaned: string[] = [];994+    for (const value of incoming) {995+      const trimmed = typeof value === "string" ? value.trim() : "";996+      if (!trimmed) continue;997+      const key = trimmed.toLowerCase();998+      if (seen.has(key)) continue;999+      seen.add(key);1000+      cleaned.push(trimmed);1001+    }1002+    nextPatch.additionalUrls = cleaned.length > 0 ? cleaned : undefined;9601003   }9611004 9621005   if ("username" in patch) {

view file

src/content/index.ts+12 −7
@@ -11,7 +11,7 @@1111   observeAnchorPosition,1212   SelectorEntry,1313   showAutofillSelector,14-  showDomainConfirmToast,14+  showRememberSiteToast,1515 } from "./inject";1616 import { fillFields } from "./fill";1717 import { setupSaveDetection } from "./save";@@ -346,13 +346,18 @@346346       const entry = (entryResponse as { ok: boolean; data?: VaultEntry }).data;347347       if (entry) {348348         await handleAutofill(anchorField, usernameEl, passwordEl, entry);349-        // Offer to set page domain as default for this entry when it differs.349+        // Offer to remember the current site for this entry only if none of350+        // its known domains (primary + additional) match — otherwise the351+        // user already mapped it and the prompt would be noise.350352         const pageDomain = location.hostname.replace(/^www\./, "");351-        const entryDomain = (() => {352-          try { return new URL(entry.url ?? "").hostname.replace(/^www\./, ""); } catch { return ""; }353-        })();354-        if (entryDomain && pageDomain && entryDomain !== pageDomain) {355-          showDomainConfirmToast(pageDomain, entry.id);353+        const entryDomains = new Set<string>();354+        for (const u of [entry.url, ...(entry.additionalUrls ?? [])]) {355+          try {356+            if (u) entryDomains.add(new URL(u).hostname.replace(/^www\./, ""));357+          } catch { /* ignore unparseable */ }358+        }359+        if (pageDomain && entryDomains.size > 0 && !entryDomains.has(pageDomain)) {360+          showRememberSiteToast(pageDomain, entry.id);356361         }357362       }358363     },

view file

src/content/inject.ts+9 −9
@@ -97,11 +97,11 @@9797 }9898 9999 /**100- * Shows a bottom-of-page toast after filling credentials when the entry's101- * saved domain doesn't match the current site — offering to set the current102- * origin as the entry's default URL. Closes automatically after 9 seconds.100+ * Shows a bottom-of-page toast after filling credentials when the entry has101+ * never matched the current site — offering to add the current origin to the102+ * entry's `additionalUrls` so future visits auto-suggest it. Closes after 9s.103103  */104-export function showDomainConfirmToast(104+export function showRememberSiteToast(105105   pageDomain: string,106106   entryId: string,107107 ): void {@@ -135,18 +135,18 @@135135 136136   const bar = document.createElement("div"); bar.className = "bar";137137   const txt = document.createElement("span"); txt.className = "txt";138-  txt.textContent = `Set ${pageDomain} as default for this entry?`;138+  txt.textContent = `Remember ${pageDomain} for this entry?`;139139   const yesBtn = document.createElement("button"); yesBtn.className = "yes";140-  yesBtn.textContent = "Set default";140+  yesBtn.textContent = "Remember";141141   yesBtn.addEventListener("click", async () => {142142     await chrome.runtime.sendMessage({143-      type: "UPDATE_ENTRY",144-      payload: { id: entryId, patch: { url: location.origin } },143+      type: "ADD_ADDITIONAL_URL",144+      payload: { id: entryId, url: location.origin },145145     });146146     host.remove();147147   });148148   const noBtn = document.createElement("button"); noBtn.className = "no";149-  noBtn.textContent = "Skip";149+  noBtn.textContent = "Not now";150150   noBtn.addEventListener("click", () => host.remove());151151   bar.append(txt, yesBtn, noBtn);152152   shadow.appendChild(bar);

view file

src/popup/App.tsx+10 −4
@@ -182,9 +182,9 @@182182     document.documentElement.style.setProperty("--peach-accent", hex);183183     // Ambient surface variables — derived from the accent so surfaces shift184184     // subtly toward the accent hue rather than staying fixed warm-brown.185-    document.documentElement.style.setProperty("--peach-glow", hex + "77");186-    document.documentElement.style.setProperty("--peach-surface-tint", hex + "1A");187-    document.documentElement.style.setProperty("--peach-border-accent", hex + "66");185+    document.documentElement.style.setProperty("--peach-glow", hex + "53");186+    document.documentElement.style.setProperty("--peach-surface-tint", hex + "12");187+    document.documentElement.style.setProperty("--peach-border-accent", hex + "47");188188   }189189 190190   createEffect(() => {@@ -505,7 +505,13 @@505505       const tabDomain = currentTabDomain();506506       if (tabDomain) {507507         nextSelectedEntry = loadedEntries.find((e) => {508-          if (e.type === "login" && e.url) return (extractDomain(e.url) ?? "") === tabDomain;508+          if (e.type === "login") {509+            if (e.url && (extractDomain(e.url) ?? "") === tabDomain) return true;510+            for (const extra of e.additionalUrls ?? []) {511+              if ((extractDomain(extra) ?? "") === tabDomain) return true;512+            }513+            return false;514+          }509515           if (e.type === "passkey" && e.rpId) return e.rpId === tabDomain;510516           return false;511517         }) ?? null;

view file

src/popup/styles/popup.css+54 −12
@@ -618,6 +618,48 @@618618   margin-top: 8px;619619 }620620 621+/* Additional URLs editor revealed by clicking the URL field */622+.peach-additional-urls {623+  display: flex;624+  flex-direction: column;625+  gap: 6px;626+  margin-top: 8px;627+}628+.peach-additional-urls-label {629+  font-size: 9px;630+  letter-spacing: 0.1em;631+  text-transform: uppercase;632+  color: var(--peach-t2);633+  margin-bottom: 2px;634+}635+.peach-additional-url-row {636+  display: flex;637+  align-items: center;638+  gap: 6px;639+}640+.peach-additional-url-row .peach-inline-input { flex: 1; min-width: 0; }641+.peach-additional-url-remove {642+  background: transparent;643+  border: 1px solid var(--peach-t3);644+  color: var(--peach-t2);645+  width: 22px;646+  height: 22px;647+  border-radius: 4px;648+  cursor: pointer;649+  font-size: 14px;650+  line-height: 1;651+  display: inline-flex;652+  align-items: center;653+  justify-content: center;654+  flex-shrink: 0;655+  transition: color 120ms ease, border-color 120ms ease;656+}657+.peach-additional-url-remove:hover:not(:disabled) {658+  color: var(--peach-accent);659+  border-color: var(--peach-border-accent);660+}661+.peach-additional-urls-add { align-self: flex-start; }662+621663 /* Delete row */622664 .peach-entry-delete-row {623665   display: flex;@@ -784,19 +826,19 @@784826   background: var(--peach-accent);785827   color: rgba(0,0,0,0.78);786828   box-shadow:787-    0 1px 0 rgba(255,255,255,0.22) inset,788-    0 0 0 1px color-mix(in srgb, var(--peach-accent) 60%, transparent),829+    0 1px 0 rgba(255,255,255,0.15) inset,830+    0 0 0 1px color-mix(in srgb, var(--peach-accent) 42%, transparent),789831     0 4px 22px var(--peach-glow),790-    0 0 32px color-mix(in srgb, var(--peach-accent) 28%, transparent);832+    0 0 32px color-mix(in srgb, var(--peach-accent) 20%, transparent);791833 }792834 .peach-btn--accent:hover:not(:disabled) {793835   background: var(--peach-accent);794-  filter: brightness(1.15) saturate(1.1);836+  filter: brightness(1.12) saturate(1.07);795837   box-shadow:796-    0 1px 0 rgba(255,255,255,0.3) inset,797-    0 0 0 1px color-mix(in srgb, var(--peach-accent) 70%, transparent),798-    0 6px 28px color-mix(in srgb, var(--peach-accent) 60%, transparent),799-    0 0 40px color-mix(in srgb, var(--peach-accent) 38%, transparent);838+    0 1px 0 rgba(255,255,255,0.21) inset,839+    0 0 0 1px color-mix(in srgb, var(--peach-accent) 50%, transparent),840+    0 6px 28px color-mix(in srgb, var(--peach-accent) 42%, transparent),841+    0 0 40px color-mix(in srgb, var(--peach-accent) 27%, transparent);800842 }801843 .peach-btn--ghost { border-color: var(--peach-t3); color: var(--peach-t1); }802844 .peach-btn--subtle { color: var(--peach-t2); border-color: var(--peach-t3); }@@ -3202,10 +3244,10 @@32023244 .peach-lock-submit {32033245   width: 100%;32043246   box-shadow:3205-    0 1px 0 rgba(255,255,255,0.28) inset,3206-    0 0 0 1px color-mix(in srgb, var(--peach-accent) 55%, transparent),3207-    0 16px 44px color-mix(in srgb, var(--peach-accent) 55%, transparent),3208-    0 0 60px color-mix(in srgb, var(--peach-accent) 28%, transparent);3247+    0 1px 0 rgba(255,255,255,0.20) inset,3248+    0 0 0 1px color-mix(in srgb, var(--peach-accent) 38%, transparent),3249+    0 16px 44px color-mix(in srgb, var(--peach-accent) 38%, transparent),3250+    0 0 60px color-mix(in srgb, var(--peach-accent) 20%, transparent);32093251 }32103252 32113253 .peach-lock-secondary {

view file

src/popup/styles/vars.css+3 −3
@@ -113,9 +113,9 @@113113   --peach-t3: var(--t3);114114   --peach-accent: var(--ac);115115   --peach-accent-dim: color-mix(in srgb, var(--peach-accent) 25%, transparent);116-  --peach-glow: #F49C7077;        /* accent bloom behind buttons/illustrations — set by applyAccent() */117-  --peach-surface-tint: #F49C701A; /* accent tint on card surfaces — set by applyAccent() */118-  --peach-border-accent: #F49C7066; /* accent-tinted border for active states — set by applyAccent() */116+  --peach-glow: #F49C7053;        /* accent bloom behind buttons/illustrations — set by applyAccent() */117+  --peach-surface-tint: #F49C7012; /* accent tint on card surfaces — set by applyAccent() */118+  --peach-border-accent: #F49C7047; /* accent-tinted border for active states — set by applyAccent() */119119   --peach-breach: var(--breach);120120   --peach-health-ok: var(--health);121121   --peach-totp-tag: var(--totp-tag);

view file

src/popup/views/Entry.tsx+67 −16
@@ -25,6 +25,9 @@2525   const [showPassword, setShowPassword] = createSignal(false);2626   const [showGenerator, setShowGenerator] = createSignal(false);2727   const [copied, setCopied] = createSignal<string | null>(null);28+  // Local draft for the additional URLs list, edited only while the URL field29+  // is in inline-edit mode. Saved alongside `url` when the user confirms.30+  const [additionalUrlsDraft, setAdditionalUrlsDraft] = createSignal<string[]>([]);2831   const [showCardNumber, setShowCardNumber] = createSignal(false);2932   const [showCardCvc, setShowCardCvc] = createSignal(false);3033   const [totpTick, setTotpTick] = createSignal(0);@@ -135,6 +138,9 @@135138     setDraft(value);136139     setShowGenerator(false);137140     setConfirmDelete(false);141+    if (field === "url") {142+      setAdditionalUrlsDraft([...(props.entry?.additionalUrls ?? [])]);143+    }138144   }139145 140146   async function saveField() {@@ -152,9 +158,14 @@152158     }153159     setSaving(true);154160     try {161+      const patch: Partial<VaultEntry> = { [field]: value };162+      if (field === "url") {163+        // Sanitization layer in vault.ts trims, dedupes, and drops empties.164+        patch.additionalUrls = additionalUrlsDraft();165+      }155166       await sendMessage({156167         type: "UPDATE_ENTRY",157-        payload: { id: entry.id, patch: { [field]: value } },168+        payload: { id: entry.id, patch },158169       });159170       triggerRing();160171       setEditingField(null);@@ -441,21 +452,61 @@441452                       </div>442453                     }443454                   >444-                    <div445-                      class="peach-inline-field-wrap"446-                      onFocusOut={(event) => handleEditorFocusOut("url", event)}447-                    >448-                      <input449-                        class="peach-inline-input"450-                        type="url"451-                        placeholder="https://"452-                        value={draft()}453-                        onInput={(e) => setDraft(e.currentTarget.value)}454-                        onKeyDown={onEditKeyDown}455-                        ref={autoFocus}456-                        disabled={saving()}457-                      />458-                      <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />455+                    <div>456+                      <div457+                        class="peach-inline-field-wrap"458+                        onFocusOut={(event) => handleEditorFocusOut("url", event)}459+                      >460+                        <input461+                          class="peach-inline-input"462+                          type="url"463+                          placeholder="https://"464+                          value={draft()}465+                          onInput={(e) => setDraft(e.currentTarget.value)}466+                          onKeyDown={onEditKeyDown}467+                          ref={autoFocus}468+                          disabled={saving()}469+                        />470+                        <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />471+                      </div>472+                      <div class="peach-additional-urls">473+                        <Show when={additionalUrlsDraft().length > 0}>474+                          <span class="peach-label peach-additional-urls-label">also matches</span>475+                        </Show>476+                        <For each={additionalUrlsDraft()}>477+                          {(value, i) => (478+                            <div class="peach-additional-url-row">479+                              <input480+                                class="peach-inline-input"481+                                type="url"482+                                placeholder="https://"483+                                value={value}484+                                onInput={(e) => setAdditionalUrlsDraft((arr) => {485+                                  const next = [...arr];486+                                  next[i()] = e.currentTarget.value;487+                                  return next;488+                                })}489+                                disabled={saving()}490+                              />491+                              <button492+                                class="peach-additional-url-remove"493+                                type="button"494+                                aria-label="Remove URL"495+                                onClick={() => setAdditionalUrlsDraft((arr) => arr.filter((_, idx) => idx !== i()))}496+                                disabled={saving()}497+                              >×</button>498+                            </div>499+                          )}500+                        </For>501+                        <button502+                          class="peach-add-field-link peach-additional-urls-add"503+                          type="button"504+                          onClick={() => setAdditionalUrlsDraft((arr) => [...arr, ""])}505+                          disabled={saving()}506+                        >507+                          + add another url508+                        </button>509+                      </div>459510                     </div>460511                   </Show>461512                 </div>

view file

src/popup/views/Vault.tsx+17 −4
@@ -1,6 +1,6 @@11 import { createEffect, createMemo, createSignal, For, Show } from "solid-js";22 import type { VaultEntry } from "../../shared/types";3-import { getDisplayDomain } from "../../shared/domain";3+import { extractDomain, getDisplayDomain } from "../../shared/domain";44 import { faviconUrl } from "../../shared/favicon";55 66 function FaviconIcon(props: { domain: string; isNote: boolean; isPasskey: boolean }) {@@ -137,6 +137,19 @@137137     return "";138138   }139139 140+  function entryMatchesDomain(entry: VaultEntry, domain: string): boolean {141+    if (!domain) return false;142+    if (entry.type === "passkey") return entry.rpId === domain;143+    if (entry.type !== "login") return false;144+    // Tab domain is the eTLD+1 from extractDomain — compare against eTLD+1 of145+    // each entry URL too so subdomain entries also match.146+    if (entry.url && extractDomain(entry.url) === domain) return true;147+    for (const extra of entry.additionalUrls ?? []) {148+      if (extractDomain(extra) === domain) return true;149+    }150+    return false;151+  }152+140153   const filteredEntries = createMemo(() => {141154     let result = props.entries;142155 @@ -196,8 +209,8 @@196209     }197210     if (tabDomain) {198211       return [...result].sort((a, b) => {199-        const aMatch = entryDomain(a) === tabDomain ? 0 : 1;200-        const bMatch = entryDomain(b) === tabDomain ? 0 : 1;212+        const aMatch = entryMatchesDomain(a, tabDomain) ? 0 : 1;213+        const bMatch = entryMatchesDomain(b, tabDomain) ? 0 : 1;201214         return aMatch - bMatch;202215       });203216     }@@ -417,7 +430,7 @@417430               const isTabMatch = () =>418431                 props.currentTabDomain !== undefined &&419432                 props.currentTabDomain !== "" &&420-                entryDomain(entry) === props.currentTabDomain;433+                entryMatchesDomain(entry, props.currentTabDomain);421434               const isSelected = () => selectedIds().has(entry.id);422435 423436               return (

view file

src/shared/types.ts+8 −0
@@ -31,6 +31,13 @@3131   username?: string; // only for login entries3232   password?: string; // only for login entries3333   url?: string; // only for login entries34+  /**35+   * Additional sites this entry should match for autofill. Login entries only.36+   * Each value is a full origin or URL (e.g. `https://login.foo.com`).37+   * The vault domain index keys against the hostname of every entry in this38+   * list, in addition to `url`.39+   */40+  additionalUrls?: string[];3441   totp?: string; // base32 seed, stored encrypted — only for login entries3542   notes?: string;3643   breach?: number; // HIBP exposure count; 0 or undefined = clean — only for login entries@@ -145,6 +152,7 @@145152   | "RESTORE_ENTRIES"146153   | "CLEAR_VAULT_ENTRIES"147154   | "UPDATE_ENTRY"155+  | "ADD_ADDITIONAL_URL"148156   | "DELETE_ENTRY"149157   | "AUTOFILL_QUERY"150158   | "COPY"

view file