Is HyperWrite: AI Writing Assistant and Autocomplete App safe?
HyperWrite is high risk. An AI-context path asks the background for open-tab context: it lists tabs and returns up to 20 URL/title pairs after filtering. The content script attaches those to page context before the same typeahead request used for AI suggestions.…
Who publishes itOtherside AI - no other listings under this identity
Otherside AI - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Open tab URLs and titles feed HyperWrite context
An AI-context path asks the background for open-tab context: it lists tabs and returns up to 20 URL/title pairs after filtering.
The content script attaches those to page context before the same typeahead request used for AI suggestions.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You use a HyperWrite AI feature that asks for more context.
The content script requests tab context only in the AI-context path shown in the code.
The extension reads open-tab URLs and titles and attaches them to the page context.
The background component returns up to 20 filtered tab records to the content script.
- Open tab URLhttps://docs.example.com/document/d/1Q4-plan/edit (illustrative)
This can show another page you have open, including the site and path.
- Open tab titleQ4 planning notes - Example Docs (illustrative)
The page title can summarize what the other tab is about.
- Tab limit20 tabs
The extension can include multiple open pages in one context request.
- Category filteringbanking.example and 127.0.0.1 are filtered patterns (illustrative)
Some banking, health, government, utility, dating, legal, and private-network URLs are filtered before tab records are returned.
Background handler enumerates tabs and returns URL/title pairs
Readable deobfuscated excerpt
deobfuscated/background.bundle.jsconst Pf = [/\b(bank|banking|chase|wellsfargo|bofa|bankofamerica|citibank|citi|usbank|pnc|capitalone|tdbank|suntrust|bb&t|truist|regions|keybank|huntington|ally|discover|americanexpress|amex|paypal|venmo|cashapp|zelle|coinbase|binance|kraken|robinhood|etrade|schwab|fidelity|vanguard|merrill|morganstanley|goldmansachs|jpmorgan)\b/i, /\b(healthcare|health|medical|medicare|medicaid|hospital|clinic|doctor|physician|patient|mychart|epic|cerner|kaiserpermanente|mayoclinic|clevelandclinic|johnshopkins|mountsinai|cedarssinai|pharmacy|cvs|walgreens|express-scripts|caremark|anthem|bluecross|blueshield|aetna|cigna|humana|unitedhealth|centene)\b/i, /\.gov$/i, /\b(irs|dmv|ssa|socialsecurity|va|veterans|usps|fedex|ups|passport|immigration|uscis|ice|tsa|fbi|cia|nsa|dhs|state\.gov|treasury|justice|defense|dod|medicare|medicaid|tax|taxes|turbotax|hrblock|jackson-hewitt)\b/i, /\b(insurance|geico|progressive|statefarm|allstate|nationwide|farmers|travelers|liberty|mutual|usaa|metlife|prudential)\b/i, /\b(electric|gas|water|utility|utilities|comcast|xfinity|spectrum|att|verizon|tmobile|sprint|cricket|mint|billing|payment)\b/i, /\b(dating|tinder|bumble|hinge|match|eharmony|okcupid|therapy|counseling|psychiatrist|psychologist|betterhelp|talkspace|divorce|lawyer|attorney|legal)\b/i, /^(localhost|127\.0\.0\.1|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/i];function Af(t = [], e = 20) { return t.filter((t => ! function(t) { if (!t) return !1; try { const e = new URL(t), i = e.hostname + e.pathname; return Pf.some((t => t.test(i))) } catch (e) { return Pf.some((e => e.test(t))) } }(t.url))).slice(0, e).map((t => ({ url: t.url && t.url.length > 100 ? t.url.substring(0, 97) + "..." : t.url, title: t.title })))}if ("getTabContext" === t.action) { return function(t = 20) { const e = Date.now(); return Of > 0 && e - Of < 5e3 && Tf >= t ? Promise.resolve(Ef.slice(0, t)) : new Promise(((e, i) => { chrome.tabs.query({}, (n => { var r; if (null === (r = chrome.runtime) || void 0 === r ? void 0 : r.lastError) return void i(new Error(chrome.runtime.lastError.message)); const s = Af(n, t); Ef = s, Tf = t, Of = Date.now(), e(s) })) })) }(t.limit || 20).then((t => { i({ tabs: t }) })).catch((t => { i({ tabs: [] }) })), !0}Content script attaches tab records to page context before typeahead
Readable deobfuscated excerpt
deobfuscated/app.bundle.jsif (u) try { const e = yield chrome.runtime.sendMessage({ action: "getTabContext", limit: 20 }); e && e.tabs && (f = e.tabs)} catch (e) {}let p = Bd({ selectionState: u ? a : void 0, activeElement: document.activeElement, forceRefresh: c});f.length > 0 && (Array.isArray(p) ? p = [...p, { otherTabs: f}] : "object" == typeof p ? p.otherTabs = f : p = { content: p, otherTabs: f});const h = "undefined" != typeof performance ? performance.now() : Date.now();ip("typeahead_request_start", { textLength: (null == o ? void 0 : o.length) || 0, priorTextLength: (null == r ? void 0 : r.length) || 0, pageContextItems: Array.isArray(p) ? p.length : null, hasMetadata: !!d, typeaheadMetadata: t}), Ao({ typeaheadMetadata: t, text: o, pageContext: p, priorText: r, email: s, metadata: d, onFailure: () => { i(!1) }})- typeaheads.hyperwriteai.com
HyperWrite typeahead service that receives page context for AI suggestions when the typeahead request is made.
Typed page text is sent to HyperWrite typeahead
When autocomplete is active, HyperWrite reads the text you're editing plus nearby page context and sends it to its typeahead service via a FormData POST with typed text, prior text, context, metadata, and snippets.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You type into a page where the autocomplete feature is active.
The keypress handler waits for typing conditions that require a new typeahead suggestion.
The extension packages typed text and page context for a remote typeahead request.
The request includes the current text, prior text, page context, metadata, snippets, and app information.
- Text you are editingPlease revise the vendor renewal paragraph before Friday. (illustrative)
This is the content you typed or selected for an autocomplete suggestion.
- Prior textQ3 contract renewal notes and approval comments. (illustrative)
Nearby text gives extra context about what you were writing before the current cursor position.
- Page context{"content":"Quarterly planning document","url":"https://docs.example.com/document/d/1abc234"} (illustrative)
Surrounding page content and metadata can reveal where the text came from and what else was visible on the page.
- Metadata and snippets{"host":"docs.google.com","app":"Google Docs"} (illustrative)
Extra context can include app-specific metadata and locally indexed snippets used to improve suggestions.
Content script opens the typeahead port and sends writing context
Readable deobfuscated excerpt
deobfuscated/app.bundle.jsconst So = e => chrome.runtime.connect({ name: e}),Co = () => { if (vo) { mo && vo.onMessage.removeListener(mo), go && vo.onDisconnect.removeListener(go); try { vo.disconnect() } catch (e) {} } vo = So("getTypeAheads"), mo = function(e) { "complete" === e.status && (e.key && delete xo[e.key], _o(Object.assign({}, e))) }, vo.onMessage.addListener(mo), go = function(e) { if (e !== vo) return; Uo({ eventName: "typeahead_port_disconnect_handler", metadata: { documentHidden: null === document || void 0 === document ? void 0 : document.hidden, timestamp: (new Date).toISOString() } }).catch((() => {})), vo.onMessage.removeListener(mo), vo.onDisconnect.removeListener(go), vo = void 0; setTimeout((() => { (null === document || void 0 === document ? void 0 : document.hidden) ? document.addEventListener("visibilitychange", (function e() { document.hidden || (document.removeEventListener("visibilitychange", e), Co()) })): Co() }), 0) }, vo.onDisconnect.addListener(go)};Co();Background script converts the port message into the POST body
Port handler forwards fields into the typeahead call
deobfuscated/background.bundle.js"getTypeAheads" === e.name && e.onMessage.addListener((function(t) { e.postMessage({ status: "getting typeaheds" }), function({ port: t, message: e }) { pf(this, void 0, void 0, (function*() { let i = e.typeaheadMetadata; Promise.resolve().then((() => pf(this, void 0, void 0, (function*() { let t = []; const n = yield Tr(); var r; n || (t = yield(r = e.text, Qi(void 0, void 0, void 0, (function*() { var t; if (!r) return []; let e = en.search(r, 5, { suggest: !0 }); const i = nn.search({ parentid: e, index: ["content"], query: r.trim(), expand: !0, suggest: !0, limit: 6 }); let n = []; const s = null === (t = null == i ? void 0 : i[0]) || void 0 === t ? void 0 : t.result; if (null == s ? void 0 : s.length) for (let t = 0; t < s.length; t++) { const e = yield Zi.sections.get(s[t]); if (e) { const t = e.content; n.push({ text: t }) } } return n })))); const s = yield wn(); return (n ? vr : fr)({ typeaheadMetadata: i, text: e.text, title: e.title, description: e.description, pageContext: e.pageContext, priorText: e.priorText, email: e.email, app: e.app, metadata: e.metadata, similarSnippets: t, variant: null == s ? void 0 : s.variant, testCase: null == s ? void 0 : s.test_case, experimentVersion: null == s ? void 0 : s.experiment_version }) })))) })) }({ port: e, message: t })}))FormData POST to the typeahead endpoint
deobfuscated/background.bundle.jsfr = ({ typeaheadMetadata: t, text: e, title: i = "Email", description: n = "Email", pageContext: r, priorText: s, email: o = "", app: a = "", metadata: u = {}, similarSnippets: l = [], variant: c, testCase: h, experimentVersion: d}) => lr(void 0, void 0, void 0, (function*() { let f = new FormData; const v = yield Bn(); f.append("writing", e), f.append("title", i), f.append("description", n), f.append("snippets", JSON.stringify(l)), t && f.append("typeaheadMetadata", JSON.stringify(t)), f.append("website", "Superhuman"), r && f.append("pageContext", JSON.stringify(r)), s && f.append("priorText", s), a && f.append("app", a), u && f.append("metadata", JSON.stringify(u)), "string" == typeof c && c.length > 0 && f.append("variant", c), null != h && f.append("test_case", String(h)), null != d && f.append("experiment_version", String(d)), f.append("allowPartial", "true"), f.append("version", "0.0.3"); const p = new AbortController, g = setTimeout((() => p.abort()), 5e3); let y = { method: "POST", body: f, redirect: "follow", signal: p.signal, headers: { Authorization: `Bearer ${v}` } }, m = (new Date).getTime(); const _ = yield fetch(hr + "/typeaheadnsorted", y).then((t => t.json())).catch((t => "AbortError" === (null == t ? void 0 : t.name) ? { choices: [], success: !1, error: "ClientTimeout" } : ["Error generating typeahead"])).finally((() => { clearTimeout(g) })); if (o.endsWith("@othersideai.com")) { (new Date).getTime(); false } return _}))- typeaheads.hyperwriteai.com
HyperWrite typeahead service that receives typed text and page context for autocomplete suggestions.
Typeahead metrics sent to HyperWrite's API
HyperWrite's typeahead sends metrics to api.hyperwriteai.com on show/accept: acceptance method, site/app context, typeahead IDs, character counts, metadata.
A separate path sends timing data; testing missed these requests.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You view or accept a HyperWrite typeahead suggestion on a supported website.
The content script watches typeahead interactions such as Tab acceptance, right-arrow acceptance, and option toggles.
The extension prepares form-data metrics and posts them to HyperWrite's API.
The background script handles messages named logAcceptedTypeahead, logShownTypeahead, and saveMeasurements.
- Site or app contextmail.google.com
Shows which website or web app you were using when the typeahead event happened.
- Acceptance methodtab
Shows how you interacted with the suggestion, such as accepting it with Tab or the right arrow.
- Typeahead identifiergen_7f3a9c2b
Can tie a shown suggestion to a later acceptance event for the same generated suggestion.
- Character countsacceptedChars: 18
Shows how much of the suggested text was accepted or displayed during the interaction.
- Measurement countersmessageTime: 1240, keyDownCount: 6, acceptedCount: 1
Records timing and keyboard counters for typeahead use when the measurement path runs.
The endpoint and method are defined in the shipped fetch call; the traffic analysis run did not record a request body for this endpoint.
- api.hyperwriteai.com
HyperWrite API host used for authenticated typeahead acceptance, display, measurement logging, and the logged-out /logTypeaheadNoAuth fallback path.
The content script records typeahead actions and the background script posts them
API base URL used by the background script
deobfuscated/background.bundle.jslet cr, hr;cr = e.BASE_URL ? e.BASE_URL : "https://api.hyperwriteai.com/", hr = e.OVERRIDE_TYPEAHEAD_URL ? e.OVERRIDE_TYPEAHEAD_URL : "https://typeaheads.hyperwriteai.com/";The page URL is reduced to a host/app value
deobfuscated/app.bundle.jsconst z = e => { const t = new URL(e).hostname, n = N(e, t); return n || { hostname: t, host: "General" }};const Hp = window.location.href;const { host: Xp, hostname: Zp} = z(Hp);The content script sends acceptance and display messages
deobfuscated/app.bundle.jsconst Fo = ({ type: e, metadata: t, id: n, app: r }) => ho(void 0, void 0, void 0, (function*() { const o = So("logAcceptedTypeahead"); yield new Promise(((i, a) => { const l = setTimeout((() => a(new Error("timeout on log success"))), 3e3); o.postMessage({ action: "logAcceptedTypeahead", type: e, metadata: t, id: n, app: r }), clearTimeout(l) })) })), Wo = ({ type: e, app: t, metadata: n }) => ho(void 0, void 0, void 0, (function*() { const r = So("logShownTypeahead"); yield new Promise(((o, i) => { const a = setTimeout((() => i(new Error("timeout on log shown"))), 3e3); r.postMessage({ action: "logShownTypeahead", type: e, app: t, metadata: n }), clearTimeout(a) })) }));The background script posts the form data
deobfuscated/background.bundle.js"saveMeasurements" === e.name && e.onMessage.addListener((function(t) { var i; e.postMessage({ status: "saving measurements" }), i = t, lr(void 0, void 0, void 0, (function*() { const t = cr + "/saveTypeAheadMeasurements"; let e = new FormData; const n = yield Bn(); return (null == i ? void 0 : i.messageTime) && e.append("messageTime", i.messageTime), (null == i ? void 0 : i.keyDownCount) && e.append("keyDownCount", i.keyDownCount), (null == i ? void 0 : i.appName) && e.append("platform", i.appName), (null == i ? void 0 : i.acceptedCount) && e.append("acceptedCount", i.acceptedCount), (null == i ? void 0 : i.notAcceptedCount) && e.append("notAcceptedCount", i.notAcceptedCount), yield fetch(t, { method: "POST", body: e, headers: { Authorization: `Bearer ${n}` } }).then((t => {})).catch((t => ({}))) }))})), "logAcceptedTypeahead" === e.name && e.onMessage.addListener((function(t) { var i; return pf(this, void 0, void 0, (function*() { e.postMessage({ status: "getting typeaheds" }); const n = yield Tr(); ! function(t, e) { var i, n, r, s, o; pf(this, void 0, void 0, (function*() { try { const a = yield vf(), u = yield Bn().catch((() => null)), l = u ? Sf(u)["https://example.com/userid"] : null, c = chrome.runtime.getManifest(); cf.capture("typeahead_accepted", { anonymous_extension_id: a, app: t.app || null, accept_type: t.type || null, generation_id: (null === (i = t.metadata) || void 0 === i ? void 0 : i.generation_id) || t.id || null, rank_id: null !== (r = null === (n = t.metadata) || void 0 === n ? void 0 : n.rank_id) && void 0 !== r ? r : null, accepted_chars: null !== (o = null === (s = t.metadata) || void 0 === s ? void 0 : s.acceptedChars) && void 0 !== o ? o : null, user_id: l ? String(l) : null, logged_out_typeahead: Boolean(e), extension_version: c.version, manifest_version: c.manifest_version, browser: "chromium", source: "chrome_extension" }, { send_instantly: !0 }) } catch (t) {} })) }(t, n), n ? yield(({ event: t, generation_id: e, app: i, metadata: n = {}, display_type: r, accept_type: s }) => lr(void 0, void 0, void 0, (function*() { const o = new FormData; return o.append("event", t), o.append("anonymous_extension_id", yield dr()), o.append("generation_id", e || ""), o.append("app", i || "unknown"), o.append("metadata", JSON.stringify(n || {})), r && o.append("display_type", r), s && o.append("accept_type", s), yield fetch(cr + "/logTypeaheadNoAuth", { method: "POST", body: o }).then((t => t.json())).catch((t => ({}))) })))({ event: "accepted", generation_id: (null === (i = t.metadata) || void 0 === i ? void 0 : i.generation_id) || t.id, app: t.app, metadata: t.metadata, accept_type: t.type }) : yield(({ type: t, metadata: e, app: i, id: n }) => lr(void 0, void 0, void 0, (function*() { let r = new FormData; const s = yield Bn(); r.append("type", t), void 0 !== e && r.append("metadata", JSON.stringify(e)), r.append("app", i), null != n && r.append("id", n); let o = { method: "POST", body: r, redirect: "follow", headers: { Authorization: `Bearer ${s}` } }; return yield fetch(cr + "/logAcceptedTypeahead", o).then((t => t.json())).catch((t => ["Error saving"])) })))({ type: t.type, metadata: t.metadata, app: t.app, id: t.id }) }))})), "logShownTypeahead" === e.name && e.onMessage.addListener((function(t) { return pf(this, void 0, void 0, (function*() { e.postMessage({ status: "getting typeaheds" }), (yield Tr()) || (yield(({ type: t, app: e, metadata: i }) => lr(void 0, void 0, void 0, (function*() { let n = new FormData; const r = yield Bn(); n.append("type", t), n.append("app", e), n.append("metadata", JSON.stringify(i)); let s = { method: "POST", body: n, redirect: "follow", headers: { Authorization: `Bearer ${r}` } }; return yield fetch(cr + "/logShownTypeahead", s).then((t => t.json())).catch((t => ["Error saving"])) })))({ type: t.type, app: t.app, metadata: t.metadata })) }))}));