Skip to content

Commit a3115c8

Browse files
committed
docs: browser-extension v1 design spec
Extension reads retailer product pages in the creator's own browser (residential IP) to sidestep AWS-IP bot-blocking. Token auth (Bearer), popup + injected button, generic JSON-LD/OG extraction across the 5 retailers. Backend adds an extension_tokens table + Bearer auth in RequireUser + a token-mint endpoint; reuses existing add-item path.
1 parent dd0ebff commit a3115c8

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# shoplit browser extension — v1 design
2+
3+
**Date:** 2026-05-24
4+
**Status:** Approved (brainstorm) → pending implementation plan
5+
6+
## Problem
7+
8+
Creators add products to a cart by pasting a retailer URL; the server fetches
9+
OpenGraph/JSON-LD metadata (title/image/price). In production this fails:
10+
shoplit's API runs on an AWS host whose datacenter IP is blocked by retailer
11+
bot-protection (Myntra serves a challenge page, Nykaa returns 403, Amazon omits
12+
`og:image`). Pasting share-text now lifts the title client-side, but **images
13+
and price still can't be fetched server-side**.
14+
15+
A browser extension solves this at the root: it runs in the creator's own
16+
browser — residential IP, logged-in retailer session — so it reads the live
17+
product page DOM exactly as a real shopper's browser sees it. No IP block, no
18+
third-party scraping service.
19+
20+
## Goals
21+
22+
- Add a product to one of the creator's shoplit carts directly from a retailer
23+
product page (Nykaa, Myntra, Amazon, Flipkart, AJIO), capturing title, image,
24+
price, and the canonical URL.
25+
- Two entry points: the toolbar popup and an injected on-page button.
26+
- Authenticate the extension to the shoplit API without relying on cross-site
27+
cookies.
28+
29+
## Non-goals (v2+)
30+
31+
- Chrome Web Store publishing (v1 is loaded unpacked for dev/testing).
32+
- Firefox / Safari ports (build MV3, which Edge also runs).
33+
- Bulk / wishlist import; variant and image-gallery selection.
34+
- Usage analytics on the extension.
35+
- A "connected devices" management UI in shoplit settings (token revocation
36+
exists in the data model; the UI is deferred).
37+
38+
## Architecture
39+
40+
Three parts: the extension, small backend additions to `shoplit-api`, and one
41+
new frontend page.
42+
43+
```
44+
Retailer product page (browser, residential IP)
45+
│ content script: extract product + inject "+ shoplit" button
46+
47+
Extension popup ── service worker ──(Bearer token)──▶ shoplit-api (api.* / shoplit.in/api)
48+
│ shows product + cart picker + note + Add GET /carts, POST /carts/{id}/items
49+
50+
Connect flow: shoplit.in/connect-extension ──(externally_connectable)──▶ service worker stores token
51+
```
52+
53+
### Component 1 — Extension (Manifest V3)
54+
55+
New top-level `extension/` directory. Plain TypeScript bundled with esbuild into
56+
three entry points. No framework.
57+
58+
- **content script** — injected on the five retailer domains (`content_scripts`
59+
matches). Responsibilities:
60+
- Extract the product (see Extraction).
61+
- Inject a floating "+ shoplit" button on product pages. Clicking it opens a
62+
small **in-page panel rendered by the content script** (MV3 can't reliably
63+
open the toolbar popup programmatically). The panel and the popup share the
64+
same add UI module and add logic — only the host surface differs.
65+
- Respond to `extractProduct` messages from the popup.
66+
- **service worker** (`background.service_worker`) — token storage
67+
(`chrome.storage.local`), all `fetch` calls to the shoplit API (host
68+
permission for `shoplit.in`), message routing between popup/content script,
69+
and `chrome.runtime.onMessageExternal` for the token handoff.
70+
- **popup** (`action`) — when not connected, shows "Connect to shoplit"; when
71+
connected, requests the current tab's extracted product from the content
72+
script, lists the creator's carts (`GET /carts`), and offers cart picker +
73+
optional note + **Add**. Minimal inline edit of title/price is allowed before
74+
adding.
75+
76+
**Manifest V3 keys:**
77+
- `manifest_version: 3`
78+
- `permissions`: `activeTab`, `scripting`, `storage`
79+
- `host_permissions`: the five retailer origins + `https://shoplit.in/*`
80+
- `content_scripts`: matches the five retailer product domains
81+
- `action`: default popup
82+
- `externally_connectable.matches`: `https://shoplit.in/*`
83+
84+
### Component 2 — Backend additions (shoplit-api)
85+
86+
- **`extension_tokens` table** (migration): `id`, `user_id` (FK), `token_hash`
87+
(sha256 of the token — never store the raw token), `created_at`,
88+
`last_used_at`, `revoked_at` (nullable). Index on `token_hash`.
89+
- **`POST /api/v1/extension/token`** — authenticated (session cookie). Generates
90+
a random token (e.g. 32 bytes base64url), stores its hash, returns the raw
91+
token once. Used by the connect page.
92+
- **Bearer auth in `RequireUser`** — the middleware first checks the session
93+
cookie (current behavior); if absent, it accepts `Authorization: Bearer
94+
<token>`, looks up the (unrevoked) token by hash, resolves the user, and
95+
updates `last_used_at`. This is additive — existing cookie flows are untouched.
96+
- **Reused, unchanged:** `GET /api/v1/carts` (picker), `POST
97+
/api/v1/carts/{id}/items` (the existing explicit-fields add path that mints the
98+
`/go` link), `GET /api/v1/me`.
99+
100+
### Component 3 — Frontend connect page
101+
102+
`shoplit.in/connect-extension` (authenticated; redirects to `/login` if not).
103+
On load it calls `POST /api/v1/extension/token`, then hands the token to the
104+
extension via `chrome.runtime.sendMessage(EXTENSION_ID, { type: "shoplit-token",
105+
token })`. If the extension isn't detected, it falls back to displaying the token
106+
as a one-time **copy-paste code** the creator pastes into the extension popup.
107+
The page warns the token grants cart access and to keep it private.
108+
109+
## Extraction strategy (generic, per-page, in order)
110+
111+
1. **JSON-LD** — parse every `<script type="application/ld+json">`, find an
112+
object (or `@graph` entry) with `@type: "Product"`; read `name`, `image`
113+
(string or array → first), `offers.price` + `offers.priceCurrency`, `url`.
114+
Nykaa, Amazon, and most retailers emit this.
115+
2. **OG / meta fallback**`og:title`/`twitter:title`, `og:image`/
116+
`twitter:image`, `product:price:amount` + `product:price:currency`.
117+
3. **Per-site selector fallback** — a small map keyed by hostname for fields the
118+
above miss (kept intentionally tiny; only added where observed necessary).
119+
120+
Retailer is classified from the hostname (a JS port of the Go `RetailerFromURL`,
121+
including the `amzn.in`/`amzn.to`/`a.co` short hosts). Canonical URL: `<link
122+
rel="canonical">``og:url``location.href`. Price is normalized to a display
123+
string (e.g. `₹590`).
124+
125+
## Add flow
126+
127+
1. Creator on a product page clicks the toolbar icon or the injected button.
128+
2. Content script extracts `{ title, imageUrl, priceText, url (canonical),
129+
retailer }`.
130+
3. Popup shows the product, fetches `GET /carts` (Bearer) for the picker.
131+
4. Creator picks a cart, optionally edits title/price or adds a note, clicks Add.
132+
5. Service worker `POST /api/v1/carts/{id}/items` (Bearer) with the fields.
133+
6. Backend runs the existing `AddProduct` path (mints the `/go` link, stores the
134+
item). Returns success.
135+
7. Popup shows "Added ✓" with a link to the cart.
136+
137+
## Error handling
138+
139+
- **Not a product page / nothing extracted** — popup shows "Couldn't find a
140+
product here" with manual fields (title/image/price) so the add never
141+
dead-ends.
142+
- **Not connected / token revoked or invalid (401)** — popup drops to the
143+
Connect state and prompts re-connect.
144+
- **API/network error on add** — inline error + retry; the extracted data stays
145+
in the popup so nothing is lost.
146+
- **Multiple JSON-LD products on a page** — prefer the one whose `url` matches
147+
the canonical URL, else the first.
148+
149+
## Security
150+
151+
- Token stored only in `chrome.storage.local` (extension-private). Backend stores
152+
only its sha256 hash; supports revocation via `revoked_at`.
153+
- `externally_connectable` restricts which web origin can message the extension
154+
to `shoplit.in`.
155+
- The connect page mints a token only for an authenticated session.
156+
- Content script footprint on retailer pages is minimal and read-only with
157+
respect to the host page (aside from the injected button element).
158+
159+
## Testing
160+
161+
- **Backend:** unit/integration tests for token mint and Bearer-auth resolution
162+
in `RequireUser`, mirroring the existing carts handler tests (testcontainers).
163+
- **Extension:** extraction unit tests against saved HTML fixtures (real Nykaa /
164+
Myntra / Amazon / Flipkart / AJIO product pages) asserting title/image/price;
165+
manual end-to-end on each retailer for the popup + injected-button + add flow.
166+
167+
## Repo layout
168+
169+
```
170+
extension/
171+
manifest.json
172+
src/
173+
content.ts # extraction + injected button
174+
service-worker.ts
175+
popup.ts / popup.html
176+
extract.ts # JSON-LD/OG/selector logic (unit-tested)
177+
retailer.ts # hostname → retailer (JS port of RetailerFromURL)
178+
api.ts # shoplit API client (Bearer)
179+
build.mjs # esbuild
180+
internal/db/migrations/000X_extension_tokens.{up,down}.sql
181+
internal/auth/… # Bearer-token resolution
182+
web/app/connect-extension/page.tsx
183+
```

0 commit comments

Comments
 (0)