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)
- Self-hosted API base URL in extension options, with validation and optional host permissions

### Changed

Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ npm run dev
npm run build:extension
```

Load `dist/extension/` as an unpacked extension. Point it at `http://localhost:3000` from the developer options page.
Load `dist/extension/` as an unpacked extension. Point it at `http://localhost:3000` (Development) or your own `https://` host (Self-hosted) from the developer options page.

### 4. Find or claim an issue

Expand All @@ -73,7 +73,7 @@ Load `dist/extension/` as an unpacked extension. Point it at `http://localhost:3

Keep the PR to one change. Match the style of nearby files. Do not commit `.env`, secrets, or `node_modules`.

Do not widen extension permissions (`storage`, host permissions) without an issue that explains why.
Do not widen default extension permissions (`storage`, `host_permissions`) without an issue that explains why. Self-hosted API hosts must stay on `optional_host_permissions`.

### 6. Test

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ npm run dev
npm run build:extension
```

Load `dist/extension/` as unpacked in Chrome or Edge. In **Details → Extension options**, set environment to Development (`http://localhost:3000`).
Load `dist/extension/` as unpacked in Chrome or Edge. In **Details → Extension options**, set environment to Development (`http://localhost:3000`) or Self-hosted with your own `https://` API URL.

Full walkthrough: [docs/development.md](docs/development.md). How to send a PR: [CONTRIBUTING.md](CONTRIBUTING.md).

Expand Down
12 changes: 11 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,17 @@ 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. Default permissions are `storage` plus host access to the hosted Curate API and `http://localhost:3000`. A self-hosted API URL is opt-in: the options page validates the URL, then `chrome.permissions.request()` asks for that origin only via `optional_host_permissions`. Default `host_permissions` stay narrow on purpose.

### Self-hosted API URLs

| Environment | URL | Host access |
|-------------|-----|-------------|
| Production | Hosted Render URL (fixed) | Default `host_permissions` |
| Development | Loopback only (`localhost`, `127.0.0.1`, `[::1]`) | Default for port 3000; optional grant for other loopback ports |
| Self-hosted | Caller-supplied `https://` URL (no wildcards, credentials, query, or fragment) | Optional grant for that origin |

`http://` is rejected except on loopback so `connect-src` can stay `https:` plus localhost variants instead of a blanket `*`. Path prefixes are kept (`https://example.com/curate` → `/curate/api/v1/...`). Validation lives in `src/shared/apiUrl.js`.

## Auth in brief

Expand Down
9 changes: 8 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ In Chrome (`chrome://extensions`) or Edge (`edge://extensions`):
2. Load unpacked.
3. Select `dist/extension/`.

Open **Details → Extension options** and set environment to **Development** (`http://localhost:3000`). That page is for developers. It is not in the popup.
Open **Details → Extension options** and choose an environment:

- **Development** — `http://localhost:3000` (other loopback ports are allowed)
- **Production** — the hosted Curate API (fixed)
- **Self-hosted** — your own `https://` Curate URL. Chrome will prompt for access to that host only.

That page is for developers and self-hosters. It is not in the popup.

After you change popup, options, or `src/shared/` code:

Expand Down Expand Up @@ -121,6 +127,7 @@ 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/apiUrl.test.js` - self-hosted API URL validation, storage resolution, manifest permissions

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
- Configurable self-hosted API URLs via optional host permissions (no rebuild)
- Light and dark theme
- Product landing page and privacy policy
- [Chrome Web Store](https://chromewebstore.google.com/detail/curate/nlkfmdiphacjgicdcagonbfnpcdjfapo)
Expand All @@ -21,7 +22,6 @@ Work that would help the project most, in no strict order:
- **Test coverage reporting** — a baseline and a documented way to run coverage locally
- **Clearer empty and error states** in the popup, labeled as `good first issue` when they are small enough
- **Token handling** — shorter-lived JWTs or a revocation path (see [security-audit.md](security-audit.md))
- **Self-hosted API URLs** — `optional_host_permissions` so a custom host does not require a rebuild
- **Contributor onboarding** — keep `good first issue` items specific (file paths and acceptance criteria)

## Considering
Expand Down
10 changes: 5 additions & 5 deletions 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` + scoped default host permissions; self-hosted hosts are optional |
| 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 @@ -24,12 +24,13 @@
| `storage` | Persist auth token, theme, API URL preference |
| `host_permissions` (production URL) | HTTPS API calls to deployed Curate backend |
| `host_permissions` (localhost) | Local development only |
| `optional_host_permissions` | User-granted access to one self-hosted API origin |

## Findings

### Low - Static host permissions for custom API URLs
### Resolved - Custom API URLs no longer need a rebuild

Users who set a custom API base URL in options must also add that origin to `host_permissions` in `manifest.json` and rebuild. Documented in options UI and store readiness doc.
Self-hosted hosts are requested at runtime with `optional_host_permissions`. Default `host_permissions` stay limited to the hosted API and localhost. URLs are validated in `src/shared/apiUrl.js` (scheme required, no wildcards, `http://` only on loopback).

### Low - JWT in local storage

Expand All @@ -50,8 +51,7 @@ Users re-authenticate after JWT expiry.
## Recommendations (future, not blocking)

1. Optional refresh tokens + server revocation list.
2. `optional_host_permissions` workflow for self-hosted API URLs.
3. Automated extension E2E tests in CI.
2. Automated extension E2E tests in CI.

## Web application

Expand Down
3 changes: 2 additions & 1 deletion docs/store-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ The extension **does not**:
Copy into store submission:

> **storage** - Saves your login token and extension preferences on your device.
> **host_permissions** - Allows the extension to sync bookmarks with the Curate server.
> **host_permissions** - Allows the extension to sync bookmarks with the hosted Curate server and with localhost during development.
> **optional host permissions** - If you point the extension at your own Curate server, Chrome asks for access to that host only. Everyday installs never receive that grant.

## Privacy policy requirements

Expand Down
28 changes: 24 additions & 4 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,29 @@ Output: `dist/extension/`

1. Copy `.env.example` → `.env` and set `JWT_SECRET`.
2. Start the web/API server: `npm run dev`
3. Open extension options from `chrome://extensions` (Details, then Extension options) and set environment to **Development** (`http://localhost:3000`).
3. Open extension options from `chrome://extensions` (Details, then Extension options) and set environment to **Development** (`http://localhost:3000`), or **Self-hosted** with your own `https://` API URL.
4. Rebuild after source changes: `npm run build:extension`, then reload the extension.

API connection (environment and base URL) is developer-only. Open it from `chrome://extensions` (Details, then Extension options). It is not shown in the popup.
API connection (environment and base URL) is for developers and self-hosters. Open it from `chrome://extensions` (Details, then Extension options). It is not shown in the popup.

### Self-hosted API

You do not need to edit `manifest.json` or rebuild to point the extension at your own Curate server.

1. Load the unpacked extension (store build or `dist/extension/`).
2. Open **Details → Extension options**.
3. Choose **Self-hosted**.
4. Enter a full URL with a scheme, for example `https://curate.example.com`.
5. Save. Chrome or Edge will ask for permission to reach that host only.

Rules the options page enforces:

- Scheme is required (`https://`, or `http://` only for localhost / `127.0.0.1` / `[::1]`).
- No `*` wildcards, credentials, query strings, or fragments.
- A path prefix is kept (`https://example.com/curate` calls `/curate/api/v1/...`).
- Production stays locked to the hosted API so everyday installs do not silently switch hosts.

Default `host_permissions` remain the hosted URL and `http://localhost:3000`. Extra hosts use `optional_host_permissions` plus `chrome.permissions.request()` at save time. The self-hosted server must allow `chrome-extension://` origins (the bundled CORS config already does).

## Scripts

Expand All @@ -76,6 +95,7 @@ See [docs/architecture.md](../docs/architecture.md) and [docs/development.md](..
## Permissions

- `storage` - auth token and preferences
- Host permissions - Curate API (production + localhost for dev)
- Host permissions - hosted Curate API and `http://localhost:3000`
- Optional host permissions - a self-hosted origin the user grants at runtime

No content scripts. No broad site access.
No content scripts. No `<all_urls>` or other broad default host access.
12 changes: 11 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@
"http://localhost:3000/*",
"https://curate-h0ga.onrender.com/*"
],
"optional_host_permissions": [
"https://*/*",
"https://*:*/*",
"http://localhost/*",
"http://localhost:*/*",
"http://127.0.0.1/*",
"http://127.0.0.1:*/*",
"http://[::1]/*",
"http://[::1]:*/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://curate-h0ga.onrender.com http://localhost:3000"
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https: http://localhost:* http://127.0.0.1:* http://[::1]:*"
}
}
6 changes: 6 additions & 0 deletions extension/options/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ body {
margin: 0;
}

.field input[readonly] {
background: var(--paper);
color: var(--muted);
cursor: default;
}

.hint code {
background: var(--paper);
padding: 2px 6px;
Expand Down
19 changes: 15 additions & 4 deletions extension/options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<header>
<p class="kicker">Developer</p>
<h1>API connection</h1>
<p class="subtitle">Point the extension at a local or custom Curate server. Users never see this page.</p>
<p class="subtitle">Point the extension at the hosted API, localhost, or your own Curate server. Users never see this page.</p>
</header>

<div id="status" class="status" hidden></div>
Expand All @@ -24,15 +24,26 @@ <h1>API connection</h1>
<label class="field">
<span>Environment</span>
<select name="environment" id="environment">
<option value="production">Production</option>
<option value="production">Production (hosted Curate)</option>
<option value="development">Development (localhost)</option>
<option value="selfhosted">Self-hosted</option>
</select>
</label>
<label class="field">
<span>API base URL</span>
<input name="apiBaseUrl" id="apiBaseUrl" type="url" required>
<input
name="apiBaseUrl"
id="apiBaseUrl"
type="url"
required
spellcheck="false"
autocomplete="off"
autocapitalize="none"
placeholder="https://curate.example.com"
aria-describedby="api-url-hint"
>
</label>
<p class="hint">Custom URLs must be listed in <code>host_permissions</code> inside <code>manifest.json</code>.</p>
<p class="hint" id="api-url-hint"></p>
<button class="btn btn-primary" type="submit">Save settings</button>
<button class="btn btn-secondary" type="button" id="test-connection">Test connection</button>
</form>
Expand Down
Loading
Loading