Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extension (`extension/manifest.json`).
### Added

- Open-source contributor docs and GitHub community files (issues, PRs, CI, code of conduct)
- Popup keyboard shortcuts: `Esc` closes the composer or goes back to the library; `Alt+Shift+S` opens Add a bookmark and prefills the current http(s) tab

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The public site lives in [`landing/`](landing/). Sign in, the library, and accou
- **Private by default** — links stay on your account.
- **Light and dark** theme.
- **Library first** — the home screen is your collections and bookmarks. Forms appear when you add or edit.
- **Keyboard shortcuts** — `Alt+Shift+S` saves the current tab; `Esc` closes a form or goes back. See [extension/README.md](extension/README.md).

## Tech stack

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Popup --Bearer JWT--> /api/v1 --> MongoDB
Options page (same API, developer host override)
```

There are **no content scripts**. The extension does not inject into web pages and does not read browsing history. Permissions are `storage` plus host access to the Curate API (production and localhost).
There are **no content scripts**. The extension does not inject into web pages and does not read browsing history. Permissions are `storage`, `activeTab` (current tab URL/title after a toolbar click or the add-bookmark command), plus host access to the Curate API (production and localhost).

## Auth in brief

Expand Down
2 changes: 2 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ database). It covers:
- `tests/api/bookmarks.test.js` - bookmark CRUD, ownership, validation
- `tests/api/collections.test.js` - collection CRUD, ownership, validation
- `tests/unit/validators.test.js` - `normalizeUrl`, `parseTags`, Joi schemas
- `tests/unit/shortcuts.test.mjs` - popup shortcut helpers (Esc routing, savable tab URLs)
- `tests/unit/extension-manifest.test.js` - add-bookmark command and `activeTab`

Tests run against a throwaway MongoDB started in memory by
`mongodb-memory-server` - no local MongoDB, `MONGO_URI`, or other setup is
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This is the direction of the project, not a contract. Features start as [issues]
- Accounts in the popup (register, sign in, profile, password, delete)
- `/api/v1` JSON API with Bearer JWT
- Automated API tests (`node --test`) for auth, bookmarks, and collections, running in CI
- Keyboard shortcuts in the popup (`Esc` to cancel/back, `Alt+Shift+S` to add the current tab)
- Light and dark theme
- Product landing page and privacy policy
- [Chrome Web Store](https://chromewebstore.google.com/detail/curate/nlkfmdiphacjgicdcagonbfnpcdjfapo)
Expand All @@ -30,7 +31,6 @@ These need an issue and a design discussion before a PR:

- Search and filter across bookmarks
- Import / export a personal library
- Keyboard shortcuts in the popup
- Firefox, if MV3 APIs and store review stay aligned with Chrome/Edge

Curate stays private. A public feed or social sharing is out of scope unless that decision changes in an issue.
3 changes: 2 additions & 1 deletion docs/security-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

| Area | Status | Notes |
|------|--------|-------|
| Manifest permissions | Pass | Only `storage` + scoped host permissions |
| Manifest permissions | Pass | `storage`, `activeTab`, and scoped host permissions |
| Secrets in bundle | Pass | No `.env`, JWT secret, or DB credentials in extension |
| CSP | Pass | `script-src 'self'`, no inline scripts |
| XSS / innerHTML | Mitigated | User content escaped before DOM insertion in popup/options |
Expand All @@ -22,6 +22,7 @@
| Permission | Justification |
|------------|---------------|
| `storage` | Persist auth token, theme, API URL preference |
| `activeTab` | Read the current tab URL/title after a user gesture so Add bookmark can prefill http(s) pages |
| `host_permissions` (production URL) | HTTPS API calls to deployed Curate backend |
| `host_permissions` (localhost) | Local development only |

Expand Down
3 changes: 2 additions & 1 deletion docs/store-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ The extension **does not**:
- Sell data
- Run analytics SDKs
- Inject content scripts into arbitrary pages
- Read browsing history (no `tabs` / `history` permission)
- Read browsing history (no `tabs` / `history` permission; `activeTab` only exposes the current tab after a user gesture)

## Permissions justification (for store review)

Copy into store submission:

> **storage** - Saves your login token and extension preferences on your device.
> **activeTab** - Reads the current tab title and URL after you click the toolbar icon or use the add-bookmark shortcut, so the composer can prefill http(s) pages.
> **host_permissions** - Allows the extension to sync bookmarks with the Curate server.

## Privacy policy requirements
Expand Down
14 changes: 13 additions & 1 deletion extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,21 @@ API connection (environment and base URL) is developer-only. Open it from `chrom

See [docs/architecture.md](../docs/architecture.md) and [docs/development.md](../docs/development.md).

## Keyboard shortcuts

These fit the popup's actual views. Existing mouse and touch actions are unchanged.

| Shortcut | Where | Action | Why this binding |
|----------|--------|--------|------------------|
| `Alt+Shift+S` (`Option+Shift+S` on macOS) | Browser (toolbar command) | Open the popup on **Add a bookmark** and prefill title/URL from the current tab when it is `http` or `https` | Unused by Chrome/Edge chrome. Avoids `Ctrl+T`, `Ctrl+W`, `Ctrl+L`, and `Ctrl+Shift+B` (bookmarks bar). Remap or disable at `chrome://extensions/shortcuts` or `edge://extensions/shortcuts`. |
| `Esc` | Popup | Close the add/edit composer (`addForm` / `collectionAddForm`). If no composer is open, go back from a collection or Account to the library. On the library with no form open, the browser still closes the popup. | Standard cancel/back. `preventDefault` runs only when Curate handles the key. |

`activeTab` is used so the command (and the + Add composer) can read the current tab's URL and title after a user gesture. The extension does not request `tabs` or `history`, and it does not prefill `chrome://`, `edge://`, `about:`, or other non-http(s) pages.

## Permissions

- `storage` - auth token and preferences
- `activeTab` - current tab URL and title, only after you click the toolbar icon or use the add-bookmark command
- Host permissions - Curate API (production + localhost for dev)

No content scripts. No broad site access.
No content scripts. No broad site access. No `tabs` or `history` permission.
42 changes: 41 additions & 1 deletion extension/background/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
/**
* MV3 service worker - auth coordination and install lifecycle.
* MV3 service worker - auth coordination, install lifecycle, and commands.
* Does not assume persistent execution.
*/

import {
COMMANDS,
MESSAGE_TYPES as SHORTCUT_MESSAGES,
PENDING_ADD_KEY,
createPendingAddPayload,
queryActiveTabPage,
} from '../shared/shortcuts.mjs';

const MESSAGE_TYPES = {
GET_AUTH_STATE: 'GET_AUTH_STATE',
CLEAR_AUTH: 'CLEAR_AUTH',
};

function pendingStorage() {
return chrome.storage.session || chrome.storage.local;
}

async function stashPendingAddBookmark(page) {
await pendingStorage().set({
[PENDING_ADD_KEY]: createPendingAddPayload(page),
});
}

chrome.commands.onCommand.addListener(async (command) => {
if (command !== COMMANDS.ADD_BOOKMARK) return;

const page = await queryActiveTabPage(chrome.tabs);
await stashPendingAddBookmark(page);

try {
await chrome.action.openPopup();
} catch {
// Older Chromium, or the popup is already visible.
}

try {
await chrome.runtime.sendMessage({
type: SHORTCUT_MESSAGES.OPEN_ADD_BOOKMARK,
...page,
});
} catch {
// No listener when the popup is closed; boot reads storage instead.
}
});

chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install' || details.reason === 'update') {
console.info('[Curate] Extension installed/updated:', details.reason);
Expand Down
12 changes: 11 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,18 @@
"type": "module"
},
"permissions": [
"storage"
"storage",
"activeTab"
],
"commands": {
"add-bookmark": {
"suggested_key": {
"default": "Alt+Shift+S",
"mac": "Alt+Shift+S"
},
"description": "Add a bookmark from the current tab"
}
},
"host_permissions": [
"http://localhost:3000/*",
"https://curate-h0ga.onrender.com/*"
Expand Down
15 changes: 15 additions & 0 deletions extension/options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ <h1>API connection</h1>
<button class="btn btn-secondary" type="button" id="test-connection">Test connection</button>
</form>
</section>

<section class="panel">
<h2>Keyboard shortcuts</h2>
<p class="hint">Popup shortcuts are listed in the extension Account view. The save command can be remapped or disabled from <code>chrome://extensions/shortcuts</code> or <code>edge://extensions/shortcuts</code>.</p>
<dl class="shortcut-list">
<div>
<dt><kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd></dt>
<dd>Add a bookmark from the current tab (Option+Shift+S on a Mac).</dd>
</div>
<div>
<dt><kbd>Esc</kbd></dt>
<dd>Close the add or edit form, or return to the library.</dd>
</div>
</dl>
</section>
</main>
<script type="module" src="options.js"></script>
</body>
Expand Down
59 changes: 59 additions & 0 deletions extension/popup/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ html[data-theme="dark"] {

[hidden] { display: none !important; }

.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

body {
margin: 0;
width: var(--popup-w);
Expand Down Expand Up @@ -467,6 +479,53 @@ body {
font-weight: 400;
}

.hint code {
font-size: 0.78rem;
background: var(--paper);
padding: 1px 5px;
border-radius: 6px;
}

.shortcut-list {
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
}

.shortcut-list div {
display: flex;
flex-direction: column;
gap: 4px;
}

.shortcut-list dt {
font-weight: 700;
color: var(--ink);
}

.shortcut-list dd {
margin: 0;
font-size: 0.82rem;
color: var(--muted);
font-weight: 400;
}

kbd {
display: inline-block;
min-width: 1.4em;
padding: 2px 6px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--paper);
color: var(--ink);
font-family: inherit;
font-size: 0.78rem;
font-weight: 700;
line-height: 1.3;
text-align: center;
}

.danger-panel {
border-color: rgba(194, 65, 59, 0.2);
}
26 changes: 21 additions & 5 deletions extension/popup/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
</header>

<div id="status" class="status" hidden></div>
<div id="shortcut-announce" class="visually-hidden" aria-live="polite"></div>

<section id="view-loading" class="view">
<p class="muted center">Loading…</p>
Expand Down Expand Up @@ -77,7 +78,7 @@ <h1 class="view-title" id="auth-title">Sign in</h1>
<p class="kicker">Library</p>
<p class="user-line" id="user-line"></p>
</div>
<button class="btn btn-primary btn-sm" type="button" id="show-add-form">+ Add</button>
<button class="btn btn-primary btn-sm" type="button" id="show-add-form" aria-keyshortcuts="Alt+Shift+S" title="Add a bookmark (Alt+Shift+S)">+ Add</button>
</div>

<form id="add-form" class="add-form" hidden>
Expand Down Expand Up @@ -124,7 +125,7 @@ <h1 class="view-title" id="auth-title">Sign in</h1>

<div class="row-actions">
<button class="btn btn-primary btn-sm" type="submit" id="add-submit">Save</button>
<button class="btn btn-secondary btn-sm" type="button" id="cancel-add">Cancel</button>
<button class="btn btn-secondary btn-sm" type="button" id="cancel-add" aria-keyshortcuts="Escape" title="Cancel (Esc)">Cancel</button>
</div>
</form>

Expand Down Expand Up @@ -177,7 +178,7 @@ <h3>No bookmarks yet</h3>
</section>

<section id="view-collection" class="view" hidden>
<button type="button" class="back-btn" id="collection-back">Back to library</button>
<button type="button" class="back-btn" id="collection-back" aria-keyshortcuts="Escape" title="Back to library (Esc)">Back to library</button>
<div class="toolbar">
<div>
<p class="kicker">Collection</p>
Expand Down Expand Up @@ -206,7 +207,7 @@ <h1 class="view-title" id="collection-title"></h1>
</label>
<div class="row-actions">
<button class="btn btn-primary btn-sm" type="submit" id="collection-add-submit">Save</button>
<button class="btn btn-secondary btn-sm" type="button" id="cancel-collection-add">Cancel</button>
<button class="btn btn-secondary btn-sm" type="button" id="cancel-collection-add" aria-keyshortcuts="Escape" title="Cancel (Esc)">Cancel</button>
</div>
</form>

Expand All @@ -224,7 +225,7 @@ <h3>This collection is empty</h3>
</section>

<section id="view-settings" class="view" hidden>
<button type="button" class="back-btn" id="settings-back">Back to library</button>
<button type="button" class="back-btn" id="settings-back" aria-keyshortcuts="Escape" title="Back to library (Esc)">Back to library</button>
<h1 class="view-title">Account</h1>
<p class="view-subtitle" id="settings-user-line"></p>

Expand Down Expand Up @@ -275,6 +276,21 @@ <h2 class="section-title">Change password</h2>
<button class="btn btn-primary btn-sm" type="submit">Update password</button>
</form>

<section class="settings-block" aria-labelledby="shortcuts-heading">
<h2 class="section-title" id="shortcuts-heading">Keyboard shortcuts</h2>
<p class="hint">These work in the popup. Mouse and touch still work the same way. Remap or disable the save command from <code>chrome://extensions/shortcuts</code> or <code>edge://extensions/shortcuts</code>.</p>
<dl class="shortcut-list">
<div>
<dt><kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd></dt>
<dd>Add a bookmark from the current tab. On a Mac this is Option+Shift+S.</dd>
</div>
<div>
<dt><kbd>Esc</kbd></dt>
<dd>Close the add or edit form, or return to the library. On the library with no form open, closes the popup.</dd>
</div>
</dl>
</section>

<div class="settings-block">
<button class="btn btn-secondary" type="button" id="logout-btn">Log out</button>
</div>
Expand Down
Loading
Loading