20 Commits
Author SHA1 Message Date
mirivlad a8c58ff584 fix: update browser extension brand icon 2026-07-15 19:17:18 +08:00
mirivlad 7ff31dd308 docs: add historical release notes 2026-07-15 07:50:17 +08:00
mirivlad ac268e5c57 docs: require human release notes 2026-07-15 07:42:02 +08:00
mirivlad f00deb3d2a release: prepare browser extension v2.0.5 2026-07-15 02:57:11 +08:00
mirivlad 8597e3b12f fix: remove unsupported Firefox permission 2026-07-15 02:53:49 +08:00
mirivlad af629e9864 release: prepare browser extension v2.0.4 2026-07-15 00:01:15 +08:00
mirivlad d6c6ca2cc5 fix: localize popup operation errors 2026-07-14 22:00:24 +08:00
mirivlad 9a96883c87 fix: style extension language selector 2026-07-14 08:00:45 +08:00
mirivlad a80a2b55e6 docs: clarify browser release tagging 2026-07-13 05:03:14 +08:00
mirivlad 4dd87dc8cc build: standardize browser GitHub releases 2026-07-13 05:02:04 +08:00
mirivlad 057b63a946 docs: point Chromium extension to GitHub 2026-07-13 03:58:15 +08:00
mirivlad 39e93e0987 chore: prepare Firefox release v2.0.3 2026-07-12 22:33:47 +08:00
mirivlad 8315d49a58 feat: publish Firefox releases to GitHub 2026-07-12 22:30:04 +08:00
mirivlad 7a278a162a feat: host Firefox updates on GitHub Releases 2026-07-12 22:28:37 +08:00
mirivlad a5751aaace test: cover Firefox GitHub release metadata 2026-07-12 22:27:45 +08:00
mirivlad 8f5438fffd docs: plan GitHub Firefox update releases 2026-07-12 22:25:16 +08:00
mirivlad bfc729affd docs: add Firefox release bootstrap requirement 2026-07-12 22:22:36 +08:00
mirivlad 0c15fe259d docs: design GitHub Firefox update releases 2026-07-12 22:18:35 +08:00
mirivlad f39550830e build: declare browser extension AGPL license 2026-07-12 21:45:44 +08:00
mirivlad 9ea4d68896 docs: prepare browser extension alpha release 2026-07-12 21:23:01 +08:00
32 changed files with 948 additions and 42 deletions
+17
View File
@@ -0,0 +1,17 @@
Verstak Browser Extension
Copyright (C) 2026 Verstak contributors
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
The complete license text is available at:
https://www.gnu.org/licenses/agpl-3.0.html
SPDX-License-Identifier: AGPL-3.0-or-later
+71 -5
View File
@@ -1,4 +1,4 @@
# verstak-browser-extension # Verstak Browser Extension
Verstak Browser Extension captures pages, selected text, links, and selected Verstak Browser Extension captures pages, selected text, links, and selected
files and sends them to a local Verstak browser inbox receiver. files and sends them to a local Verstak browser inbox receiver.
@@ -7,6 +7,8 @@ The extension does not know Notes, Files, Activity, or Journal internals. It
only sends capture events through the public local receiver protocol. If the only sends capture events through the public local receiver protocol. If the
receiver is offline, captures stay in the extension pending queue. receiver is offline, captures stay in the extension pending queue.
> **Alpha software.** Use with a matching Verstak Desktop alpha release.
## Build ## Build
```bash ```bash
@@ -20,14 +22,62 @@ Build output:
- `dist/chromium` - `dist/chromium`
- `dist/firefox` - `dist/firefox`
## Firefox Release Load `dist/chromium` as an unpacked extension in Chromium-based browsers, or
load `dist/firefox` temporarily in Firefox during development.
## Passive domain activity
Passive tracking is **off by default**. On first use the extension explains
what it records; the user must explicitly enable it in extension settings.
When enabled, it observes only the focused browser's active tab and sends
bounded aggregate intervals as a canonical domain name plus duration. It never
sends URL paths, page titles, page text, selected text, keystrokes, navigation
history or inactive-tab time. The settings screen has an exclusion list for
domains such as `youtube.com` or `x.com`.
Manual “Send page”, selection, link and file actions are separate. They create
Browser Inbox captures; they do not create a workspace or a journal entry.
## Firefox download and updates
The signed XPI is published on the [GitHub Releases page](https://github.com/mirivlad/verstak-browser-extension/releases).
Download the Firefox asset named `verstak-firefox-<version>.xpi` from the latest
release and open it in Firefox to install it.
After the first public release, installed copies check GitHub Releases for
updates through the release's `updates.json` asset. During the alpha phase each
published release is the current update channel.
## Firefox release publishing
Firefox signing uses `web-ext` and AMO credentials from an env file. The script Firefox signing uses `web-ext` and AMO credentials from an env file. The script
requires `WEB_EXT_API_PROXY`; AMO upload and approval polling run through that requires `WEB_EXT_API_PROXY`; AMO upload and approval polling run through that
proxy. proxy.
Create the signed XPI locally, without publishing it:
```bash ```bash
VERSTAK_BROWSER_ENV=/home/mirivlad/git/verstak/.env npm run release:firefox VERSTAK_BROWSER_ENV=/path/to/.env npm run release:firefox
```
Publish the signed XPI and `updates.json` as the current GitHub Release:
```bash
VERSTAK_BROWSER_ENV=/path/to/.env npm run publish:github
```
The publisher reads the version from `package.json` and requires an
authenticated GitHub CLI plus a clean local `main` equal to `origin/main`. It
creates and pushes the version tag if needed, then creates or updates the
GitHub Release. Re-running it for the same tag replaces the XPI and
`updates.json` assets. `npm run publish:firefox` remains as a compatible alias
for the same Firefox publishing flow.
For an explicit version check, pass the current tag after `--`:
```bash
VERSTAK_BROWSER_ENV=/path/to/.env npm run publish:github -- v2.0.6
``` ```
Release output: Release output:
@@ -35,8 +85,19 @@ Release output:
- `release/firefox/verstak-firefox-<version>.xpi` - `release/firefox/verstak-firefox-<version>.xpi`
- `release/firefox/updates.json` - `release/firefox/updates.json`
The XPI is signed as an unlisted/self-distributed Firefox extension. Build and The XPI is signed as an unlisted/self-distributed Firefox extension; GitHub
release artifacts are local outputs and are not committed. Releases distribute that signed file. Build and release artifacts are local
outputs and are not committed.
## Reproducible local package
```bash
npm run release:package -- v0.1.0-alpha.1
```
This runs the tests and build, then writes unsigned Chromium and Firefox source
packages to `release/` with a `SHA256SUMS` file. Use `release:firefox` above
when an AMO-signed XPI is required.
## Manual Check ## Manual Check
@@ -109,3 +170,8 @@ Expected success response:
```json ```json
{ "status": "accepted", "captureId": "uuid-or-generated-id" } { "status": "accepted", "captureId": "uuid-or-generated-id" }
``` ```
## License
Copyright © 2026 Verstak contributors. Licensed under
[GNU AGPLv3 or later](LICENSE).
+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Verstak Bridge", "name": "Verstak Bridge",
"version": "2.0.2", "version": "2.0.6",
"description": "Send pages, selections, links, and files to the local Verstak browser inbox.", "description": "Send pages, selections, links, and files to the local Verstak browser inbox.",
"author": "Verstak", "author": "Verstak",
"homepage_url": "https://git.mirv.top/verstak/verstak-browser-extension", "homepage_url": "https://github.com/mirivlad/verstak-browser-extension",
"permissions": ["alarms", "contextMenus", "idle", "storage", "tabs", "windows"], "permissions": ["alarms", "contextMenus", "idle", "storage", "tabs", "windows"],
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"], "host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
"background": { "background": {
@@ -0,0 +1,329 @@
# GitHub Firefox Updates Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Publish signed Firefox XPI releases to GitHub Releases and let installed alpha extensions discover them through a GitHub-hosted update manifest.
**Architecture:** Firefox keeps one immutable `update_url` targeting the current GitHub Release's `updates.json` asset. A small Node helper owns the tag, asset URL and update-manifest formatting so it has focused unit tests. The existing local signing command remains local; an explicit publishing command uploads its two artifacts to the public GitHub release.
**Tech Stack:** Manifest V2 JSON, Node.js CommonJS tests, Bash, `web-ext`, GitHub CLI (`gh`).
## Global Constraints
- Publish repository: `mirivlad/verstak-browser-extension`.
- Firefox addon ID: `verstak-bridge@verstak.app`.
- Initial GitHub release: `v2.0.3`; package and both browser manifests are version `2.0.3`.
- Every alpha GitHub release is ordinary/latest, not a prerelease.
- XPI remains Mozilla-signed as an unlisted/self-distributed addon before upload.
- `npm run release:firefox` remains a local sign-and-package action.
- `npm run publish:firefox` is the only command allowed to create or modify a GitHub Release.
- Generated `release/` output remains untracked.
---
### Task 1: Make GitHub update metadata testable
**Files:**
- Create: `scripts/firefox-github-release.js`
- Create: `scripts/test-firefox-github-release.js`
- Modify: `package.json:8-15`
**Interfaces:**
- Produces `releaseTag(version)`, `releaseAssetURL(version, assetName)`, and `updateManifest(addonID, version, assetName)` from `scripts/firefox-github-release.js`.
- `updateManifest` returns the object written as `updates.json` and uses the fixed GitHub repository.
- Later shell scripts invoke `node scripts/firefox-github-release.js write-updates <addon-id> <version> <asset-name> <output-path>`.
- [ ] **Step 1: Write the failing metadata test**
Create `scripts/test-firefox-github-release.js` with:
```js
const assert = require('assert');
const release = require('./firefox-github-release');
const version = '2.0.3';
const asset = 'verstak-firefox-2.0.3.xpi';
assert.equal(release.releaseTag(version), 'v2.0.3');
assert.equal(
release.releaseAssetURL(version, asset),
'https://github.com/mirivlad/verstak-browser-extension/releases/download/v2.0.3/verstak-firefox-2.0.3.xpi',
);
assert.deepEqual(release.updateManifest('verstak-bridge@verstak.app', version, asset), {
addons: {
'verstak-bridge@verstak.app': {
updates: [{
version: '2.0.3',
update_link: 'https://github.com/mirivlad/verstak-browser-extension/releases/download/v2.0.3/verstak-firefox-2.0.3.xpi',
}],
},
},
});
console.log('Firefox GitHub release metadata tests passed');
```
- [ ] **Step 2: Run the focused test and verify it fails**
Run: `node scripts/test-firefox-github-release.js`
Expected: failure because `scripts/firefox-github-release.js` does not yet exist.
- [ ] **Step 3: Implement the metadata helper**
Create `scripts/firefox-github-release.js` with these exact exports and CLI:
```js
const fs = require('fs');
const REPOSITORY = 'mirivlad/verstak-browser-extension';
function releaseTag(version) { return `v${version}`; }
function releaseAssetURL(version, assetName) {
return `https://github.com/${REPOSITORY}/releases/download/${releaseTag(version)}/${assetName}`;
}
function updateManifest(addonID, version, assetName) {
return { addons: { [addonID]: { updates: [{ version, update_link: releaseAssetURL(version, assetName) }] } } };
}
if (require.main === module) {
const [, , command, addonID, version, assetName, outputPath] = process.argv;
if (command !== 'write-updates' || !addonID || !version || !assetName || !outputPath) process.exitCode = 2;
else fs.writeFileSync(outputPath, `${JSON.stringify(updateManifest(addonID, version, assetName), null, 2)}\n`);
}
module.exports = { REPOSITORY, releaseTag, releaseAssetURL, updateManifest };
```
- [ ] **Step 4: Register and run the test**
Append `node scripts/test-firefox-github-release.js` to the existing `test`
script in `package.json`, then run:
```bash
npm test
```
Expected: all existing tests and `Firefox GitHub release metadata tests passed`.
- [ ] **Step 5: Commit and push**
```bash
git add package.json scripts/firefox-github-release.js scripts/test-firefox-github-release.js
git commit -m "test: cover Firefox GitHub release metadata"
git push origin main
```
### Task 2: Point Firefox and local release artifacts at GitHub
**Files:**
- Modify: `firefox/manifest.json:8-17`
- Modify: `scripts/release-firefox-xpi.sh:29-70`
- Test: `scripts/test-firefox-github-release.js`
**Interfaces:**
- Firefox reads `https://github.com/mirivlad/verstak-browser-extension/releases/latest/download/updates.json`.
- `release-firefox-xpi.sh` invokes the Task 1 helper to write a versioned update manifest.
- It continues producing `release/firefox/verstak-firefox-<version>.xpi` and `release/firefox/updates.json` locally.
- [ ] **Step 1: Extend the failing test with the stable manifest URL**
Add this assertion to `scripts/test-firefox-github-release.js` before its final
success message:
```js
const firefoxManifest = require('../firefox/manifest.json');
assert.equal(
firefoxManifest.browser_specific_settings.gecko.update_url,
'https://github.com/mirivlad/verstak-browser-extension/releases/latest/download/updates.json',
);
```
- [ ] **Step 2: Run the focused test and verify it fails**
Run: `node scripts/test-firefox-github-release.js`
Expected: assertion failure because the manifest still uses `mirv.top`.
- [ ] **Step 3: Replace the update endpoint and JSON writer**
In `firefox/manifest.json`, replace only the Gecko `update_url` value with:
```json
"update_url": "https://github.com/mirivlad/verstak-browser-extension/releases/latest/download/updates.json"
```
In `scripts/release-firefox-xpi.sh`, remove `UPDATE_BASE_URL` and replace the
heredoc that writes `updates.json` with:
```bash
node scripts/firefox-github-release.js write-updates \
"$ADDON_ID" "$VERSION" "$RELEASE_XPI" "$RELEASE_DIR/updates.json"
```
- [ ] **Step 4: Run targeted checks**
Run:
```bash
node scripts/test-firefox-github-release.js
bash -n scripts/release-firefox-xpi.sh
npm test
npm run build
```
Expected: all commands succeed; `dist/firefox/manifest.json` contains the
GitHub update URL after the build.
- [ ] **Step 5: Commit and push**
```bash
git add firefox/manifest.json scripts/release-firefox-xpi.sh scripts/test-firefox-github-release.js
git commit -m "feat: host Firefox updates on GitHub Releases"
git push origin main
```
### Task 3: Add explicit GitHub publishing and public instructions
**Files:**
- Create: `scripts/publish-firefox-github-release.sh`
- Modify: `package.json:8-15`
- Modify: `README.md:42-68`
- Test: `scripts/publish-firefox-github-release.sh`
**Interfaces:**
- `npm run publish:firefox` calls `scripts/publish-firefox-github-release.sh`.
- The script runs the existing local release command, then uploads
`verstak-firefox-<version>.xpi` and `updates.json` to release tag
`v<version>` in `mirivlad/verstak-browser-extension`.
- A pre-existing tag receives asset replacement through `gh release upload --clobber`.
- [ ] **Step 1: Write the shell-contract test**
Create a Node test that reads the publish script and asserts the mandatory
commands are present:
```js
const assert = require('assert');
const fs = require('fs');
const source = fs.readFileSync('scripts/publish-firefox-github-release.sh', 'utf8');
assert.match(source, /gh auth status/);
assert.match(source, /gh release create/);
assert.match(source, /gh release upload/);
assert.match(source, /--clobber/);
assert.match(source, /--latest/);
```
Place those assertions in `scripts/test-firefox-github-release.js` and run the
test. Expected: it fails before the publish script exists.
- [ ] **Step 2: Implement an explicit idempotent publisher**
Create `scripts/publish-firefox-github-release.sh` with this behavior:
1. Set `ROOT_DIR`, `cd` to it and run `gh auth status`.
2. Run `./scripts/release-firefox-xpi.sh`.
3. Read `VERSION` from `dist/firefox/manifest.json`, set `TAG="v${VERSION}"`,
`REPOSITORY="mirivlad/verstak-browser-extension"`, and locate the two local
release assets by exact names.
4. Run `gh release view "$TAG" --repo "$REPOSITORY"`; when it is absent, run
`gh release create "$TAG" "$XPI" "$UPDATES" --repo "$REPOSITORY" --title
"Verstak Browser Extension $VERSION" --generate-notes --latest --target
"$(git rev-parse HEAD)"`.
5. When the release already exists, run `gh release upload "$TAG" "$XPI"
"$UPDATES" --repo "$REPOSITORY" --clobber`.
6. Print the GitHub release URL with `gh release view "$TAG" --repo
"$REPOSITORY" --json url --jq .url`.
- [ ] **Step 3: Register the command and document it**
Add to `package.json`:
```json
"publish:firefox": "./scripts/publish-firefox-github-release.sh"
```
Rewrite the Firefox Release README section so it documents:
- the public [GitHub Releases](https://github.com/mirivlad/verstak-browser-extension/releases) page;
- `VERSTAK_BROWSER_ENV=/path/to/.env npm run publish:firefox`;
- that signing remains unlisted with Mozilla but distribution and automatic
updates use GitHub Release assets;
- that the first `v2.0.3` publish bootstraps the `latest/download/updates.json`
endpoint.
- [ ] **Step 4: Run checks without publishing**
Run:
```bash
bash -n scripts/publish-firefox-github-release.sh
node scripts/test-firefox-github-release.js
npm test
npm run build
git diff --check
```
Expected: all commands succeed. Do not invoke `npm run publish:firefox` in this
task; it creates a public GitHub Release and needs available AMO credentials.
- [ ] **Step 5: Commit and push**
```bash
git add README.md package.json scripts/publish-firefox-github-release.sh scripts/test-firefox-github-release.js
git commit -m "feat: publish Firefox releases to GitHub"
git push origin main
```
### Task 4: Bootstrap and verify the first public update release
**Files:**
- Generated: `release/firefox/verstak-firefox-2.0.3.xpi` (untracked)
- Generated: `release/firefox/updates.json` (untracked)
**Interfaces:**
- Consumes the Task 3 `npm run publish:firefox` command and valid AMO signing credentials.
- Produces GitHub Release tag `v2.0.3` and public XPI/update-manifest assets.
- [ ] **Step 1: Verify release credentials before write operations**
Run:
```bash
gh auth status
test -n "${VERSTAK_BROWSER_ENV:-}" || test -f .env
```
Expected: authenticated GitHub CLI and a configured environment file path for
the existing AMO signing command.
- [ ] **Step 2: Publish the first release**
Run:
```bash
npm run publish:firefox
```
Expected: Mozilla signing succeeds, GitHub Release `v2.0.3` is latest, and it
contains `verstak-firefox-2.0.3.xpi` plus `updates.json`.
- [ ] **Step 3: Verify public delivery**
Run:
```bash
gh release view v2.0.3 --repo mirivlad/verstak-browser-extension --json isLatest,url,assets
curl -fsSL https://github.com/mirivlad/verstak-browser-extension/releases/latest/download/updates.json
```
Expected: the release is latest and the JSON contains addon ID
`verstak-bridge@verstak.app`, version `2.0.3`, and the versioned GitHub XPI
asset URL.
- [ ] **Step 4: Record release verification without committing assets**
Run:
```bash
git status --short
```
Expected: no generated `release/` assets are staged or committed.
@@ -0,0 +1,66 @@
# GitHub Releases for Firefox Updates
## Decision
Firefox XPI releases are published as ordinary public GitHub Releases in
`mirivlad/verstak-browser-extension`. During the alpha phase every such release
is marked as the repository's latest release, including alpha versions. There
are no other Verstak users whose automatic updates need a separate stable
channel yet.
## Update protocol
The Firefox manifest uses this stable endpoint:
```text
https://github.com/mirivlad/verstak-browser-extension/releases/latest/download/updates.json
```
Each release uploads two assets:
- `verstak-firefox-<version>.xpi`, signed by Mozilla as an unlisted addon;
- `updates.json`, whose `update_link` points to that same release tag's XPI
asset.
For version `2.0.3`, the versioned link is:
```text
https://github.com/mirivlad/verstak-browser-extension/releases/download/v2.0.3/verstak-firefox-2.0.3.xpi
```
The stable `latest/download` endpoint lets already installed extensions discover
the next version without changing the manifest on each release.
## Release commands
`npm run release:firefox` remains the local sign-and-package command. A new
explicit `npm run publish:firefox` command runs it, verifies `gh`
authentication, creates or reuses the `v<manifest-version>` GitHub Release,
and uploads the XPI plus `updates.json`. Re-running the command replaces only
those two assets, so a failed network upload can be retried safely.
Publishing requires the existing AMO credentials used for signing and a
GitHub-authenticated `gh` CLI. It does not use the invalid `mirv.top` update
endpoint.
## Bootstrap
There is currently no GitHub Release in this repository. After the code change,
the first `v2.0.3` signed release must be published before shipping a manifest
that points to `latest/download/updates.json`; until then that URL is a 404.
The current package, Chromium manifest and Firefox manifest all use version
`2.0.3`, so the initial tag and XPI asset name are unambiguous.
## Documentation
The README links users to the GitHub Releases page for downloading the signed
Firefox XPI and documents that releases are currently the auto-update channel.
## Verification
- add a focused script test for versioned GitHub URLs and generated
`updates.json`;
- run the existing extension test suite and build;
- run shell syntax validation for the publish script;
- verify a published release's assets and `latest/download/updates.json` with
unauthenticated HTTPS requests.
+4 -4
View File
@@ -1,21 +1,21 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Verstak Bridge", "name": "Verstak Bridge",
"version": "2.0.2", "version": "2.0.6",
"description": "Send pages, selections, links, and files to the local Verstak browser inbox.", "description": "Send pages, selections, links, and files to the local Verstak browser inbox.",
"author": "Verstak", "author": "Verstak",
"homepage_url": "https://git.mirv.top/verstak/verstak-browser-extension", "homepage_url": "https://github.com/mirivlad/verstak-browser-extension",
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "verstak-bridge@verstak.app", "id": "verstak-bridge@verstak.app",
"strict_min_version": "115.0", "strict_min_version": "115.0",
"update_url": "https://mirv.top/verstak/firefox/updates.json", "update_url": "https://github.com/mirivlad/verstak-browser-extension/releases/latest/download/updates.json",
"data_collection_permissions": { "data_collection_permissions": {
"required": ["none"] "required": ["none"]
} }
} }
}, },
"permissions": ["alarms", "contextMenus", "idle", "storage", "tabs", "windows", "http://127.0.0.1/*", "http://localhost/*"], "permissions": ["alarms", "contextMenus", "idle", "storage", "tabs", "http://127.0.0.1/*", "http://localhost/*"],
"background": { "background": {
"scripts": ["hostname.js", "activity-tracker.js", "protocol.js", "api.js", "queue.js", "i18n.js", "background.js"] "scripts": ["hostname.js", "activity-tracker.js", "protocol.js", "api.js", "queue.js", "i18n.js", "background.js"]
}, },
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "verstak-browser-extension", "name": "verstak-browser-extension",
"version": "2.0.2", "version": "2.0.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "verstak-browser-extension", "name": "verstak-browser-extension",
"version": "2.0.2", "version": "2.0.6",
"devDependencies": { "devDependencies": {
"web-ext": "^8.3.0" "web-ext": "^8.3.0"
} }
+7 -3
View File
@@ -1,13 +1,17 @@
{ {
"name": "verstak-browser-extension", "name": "verstak-browser-extension",
"version": "2.0.2", "version": "2.0.6",
"private": true, "private": true,
"description": "Verstak browser capture extension for Chromium and Firefox", "description": "Verstak browser capture extension for Chromium and Firefox",
"license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
"build": "node scripts/build-extension.js", "build": "node scripts/build-extension.js",
"test": "node scripts/test-hostname.js && node scripts/test-activity-tracker.js && node scripts/test-protocol.js && node scripts/test-i18n.js && node scripts/test-popup-settings.js && node scripts/test-popup-catalog-fallback.js && node scripts/test-background-i18n.js", "test": "node scripts/test-manifests.js && node scripts/test-icons.js && node scripts/test-hostname.js && node scripts/test-activity-tracker.js && node scripts/test-protocol.js && node scripts/test-i18n.js && node scripts/test-popup-settings.js && node scripts/test-popup-select-style.js && node scripts/test-popup-catalog-fallback.js && node scripts/test-background-i18n.js && node scripts/test-firefox-github-release.js",
"sign:firefox": "./scripts/sign-firefox-xpi.sh", "sign:firefox": "./scripts/sign-firefox-xpi.sh",
"release:firefox": "./scripts/release-firefox-xpi.sh" "release:firefox": "./scripts/release-firefox-xpi.sh",
"publish:firefox": "./scripts/publish-firefox-github-release.sh",
"publish:github": "./scripts/publish-github-release.sh",
"release:package": "./scripts/package-release.sh"
}, },
"devDependencies": { "devDependencies": {
"web-ext": "^8.3.0" "web-ext": "^8.3.0"
+7
View File
@@ -0,0 +1,7 @@
## Highlights
- First public Firefox release distributed and updated through GitHub Releases.
- Captures links, selected text, files, and images only when the user explicitly sends them to Verstak.
- Adds opt-in domain activity tracking, Russian localization, and secure receiver-token pairing.
**Full Changelog**: https://github.com/mirivlad/verstak-browser-extension/compare/v2.0.2...v2.0.3
+5
View File
@@ -0,0 +1,5 @@
## Highlights
- The Firefox package no longer requests a permission unsupported by Firefox.
- Popup operation errors and the language selector are localized and use the extension's visual style.
- The release includes the signed XPI and `updates.json` for GitHub-hosted Firefox updates.
+13
View File
@@ -0,0 +1,13 @@
## Highlights
- Replaced the legacy orange extension symbol with the current Verstak workbench
icon in every browser size: 16, 48, and 128 pixels.
- Chromium and Firefox packages now use the same icon source as the Verstak
desktop application.
## Главное
- Старый оранжевый знак расширения заменён на актуальный фирменный значок
Верстака во всех размерах: 16, 48 и 128 пикселей.
- Пакеты Chromium и Firefox теперь используют тот же исходный знак, что и
приложение Верстак.
+40
View File
@@ -0,0 +1,40 @@
const fs = require('fs');
const REPOSITORY = 'mirivlad/verstak-browser-extension';
function releaseTag(version) {
return `v${version}`;
}
function releaseAssetURL(version, assetName) {
return `https://github.com/${REPOSITORY}/releases/download/${releaseTag(version)}/${assetName}`;
}
function updateManifest(addonID, version, assetName) {
return {
addons: {
[addonID]: {
updates: [{
version,
update_link: releaseAssetURL(version, assetName),
}],
},
},
};
}
function writeUpdates(addonID, version, assetName, outputPath) {
fs.writeFileSync(outputPath, `${JSON.stringify(updateManifest(addonID, version, assetName), null, 2)}\n`);
}
if (require.main === module) {
const [, , command, addonID, version, assetName, outputPath] = process.argv;
if (command !== 'write-updates' || !addonID || !version || !assetName || !outputPath) {
console.error('usage: node scripts/firefox-github-release.js write-updates <addon-id> <version> <asset-name> <output-path>');
process.exitCode = 2;
} else {
writeUpdates(addonID, version, assetName, outputPath);
}
}
module.exports = { REPOSITORY, releaseTag, releaseAssetURL, updateManifest, writeUpdates };
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MAGICK="${MAGICK_BIN:-magick}"
SOURCE="$ROOT/shared/icons/verstak.svg"
if ! command -v "$MAGICK" >/dev/null; then
echo "ImageMagick is required to generate browser extension icons: $MAGICK not found" >&2
exit 1
fi
for size in 16 48 128; do
"$MAGICK" -background none "$SOURCE" -resize "${size}x${size}" \
"PNG32:$ROOT/shared/icons/icon${size}.png"
done
echo "generated browser extension icons from $SOURCE"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
VERSION="${1:-}"
if [[ -z "$VERSION" || ! "$VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
echo "usage: $0 <version>" >&2
echo "example: $0 v0.1.0-alpha.1" >&2
exit 2
fi
echo "=== verstak browser extension release $VERSION ==="
(cd "$ROOT" && npm ci --no-audit --no-fund && npm test && npm run build)
RELEASE_ROOT="$ROOT/release"
STAGING="$RELEASE_ROOT/verstak-browser-extension-$VERSION"
ARCHIVE="$RELEASE_ROOT/verstak-browser-extension-$VERSION.tar.gz"
rm -rf "$STAGING" "$ARCHIVE"
mkdir -p "$STAGING"
cp "$ROOT/README.md" "$ROOT/LICENSE" "$STAGING/"
cp -R "$ROOT/dist" "$STAGING/dist"
tar -C "$RELEASE_ROOT" -czf "$ARCHIVE" "$(basename "$STAGING")"
(cd "$RELEASE_ROOT" && sha256sum "$(basename "$ARCHIVE")" > SHA256SUMS)
echo "release archive: $ARCHIVE"
echo "checksums: $RELEASE_ROOT/SHA256SUMS"
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
REPOSITORY="mirivlad/verstak-browser-extension"
SOURCE_DIR="${VERSTAK_FIREFOX_SOURCE_DIR:-dist/firefox}"
RELEASE_DIR="${VERSTAK_FIREFOX_RELEASE_DIR:-release/firefox}"
RELEASE_NOTES_DIR="${VERSTAK_RELEASE_NOTES_DIR:-release-notes}"
GIT_BIN="${GIT_BIN:-git}"
GH_BIN="${GH_BIN:-gh}"
if ! command -v "$GH_BIN" >/dev/null; then
echo "ERROR: gh CLI is required to publish a GitHub Release" >&2
exit 1
fi
if [[ "$("$GIT_BIN" branch --show-current)" != "main" ]]; then
echo "ERROR: GitHub releases must be published from main" >&2
exit 1
fi
if [[ -n "$("$GIT_BIN" status --porcelain)" ]]; then
echo "ERROR: working tree must be clean before publishing a release" >&2
exit 1
fi
"$GH_BIN" auth status
"$GIT_BIN" fetch origin main --tags
HEAD="$("$GIT_BIN" rev-parse HEAD)"
if [[ "$HEAD" != "$("$GIT_BIN" rev-parse origin/main)" ]]; then
echo "ERROR: local main must match origin/main before publishing a release" >&2
exit 1
fi
./scripts/release-firefox-xpi.sh
VERSION="$(node -e "console.log(require('./${SOURCE_DIR}/manifest.json').version)")"
TAG="v${VERSION}"
XPI="$RELEASE_DIR/verstak-firefox-${VERSION}.xpi"
UPDATES="$RELEASE_DIR/updates.json"
for artifact in "$XPI" "$UPDATES"; do
if [[ ! -f "$artifact" ]]; then
echo "ERROR: release artifact not found: $artifact" >&2
exit 1
fi
done
if "$GIT_BIN" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
if [[ "$("$GIT_BIN" rev-parse "${TAG}^{commit}")" != "$HEAD" ]]; then
echo "ERROR: existing tag $TAG does not point at HEAD" >&2
exit 1
fi
else
"$GIT_BIN" tag -a "$TAG" -m "Release $TAG"
"$GIT_BIN" push origin "refs/tags/$TAG"
fi
if "$GH_BIN" release view "$TAG" --repo "$REPOSITORY" >/dev/null 2>&1; then
"$GH_BIN" release upload "$TAG" "$XPI" "$UPDATES" --repo "$REPOSITORY" --clobber
else
NOTES_FILE="$RELEASE_NOTES_DIR/$TAG.md"
if [[ ! -s "$NOTES_FILE" ]]; then
echo "ERROR: human-readable release notes are required: $NOTES_FILE" >&2
exit 1
fi
RELEASE_OPTIONS=(--notes-file "$NOTES_FILE" --generate-notes --latest --verify-tag)
PREVIOUS_TAG="$("$GIT_BIN" describe --tags --abbrev=0 "${HEAD}^" 2>/dev/null || true)"
if [[ -n "$PREVIOUS_TAG" ]]; then
RELEASE_OPTIONS+=(--notes-start-tag "$PREVIOUS_TAG")
fi
"$GH_BIN" release create "$TAG" "$XPI" "$UPDATES" \
--repo "$REPOSITORY" \
--title "Verstak Browser Extension $VERSION" \
"${RELEASE_OPTIONS[@]}"
fi
echo "GitHub release:"
"$GH_BIN" release view "$TAG" --repo "$REPOSITORY" --json url --jq .url
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
VERSION="$(node -p "require('$ROOT/package.json').version")"
if [[ $# -gt 1 || ( $# -eq 1 && "$1" != "v$VERSION" && "$1" != "$VERSION" ) ]]; then
echo "usage: $0 [v$VERSION]" >&2
exit 2
fi
exec "$ROOT/scripts/publish-firefox-github-release.sh"
+2 -15
View File
@@ -32,7 +32,6 @@ load_env_file "$ENV_FILE"
SOURCE_DIR="${VERSTAK_FIREFOX_SOURCE_DIR:-dist/firefox}" SOURCE_DIR="${VERSTAK_FIREFOX_SOURCE_DIR:-dist/firefox}"
ARTIFACTS_DIR="${WEB_EXT_ARTIFACTS_DIR:-web-ext-artifacts}" ARTIFACTS_DIR="${WEB_EXT_ARTIFACTS_DIR:-web-ext-artifacts}"
RELEASE_DIR="${VERSTAK_FIREFOX_RELEASE_DIR:-release/firefox}" RELEASE_DIR="${VERSTAK_FIREFOX_RELEASE_DIR:-release/firefox}"
UPDATE_BASE_URL="${VERSTAK_FIREFOX_UPDATE_BASE_URL:-https://mirv.top/verstak/firefox}"
./scripts/sign-firefox-xpi.sh ./scripts/sign-firefox-xpi.sh
@@ -54,20 +53,8 @@ mkdir -p "$RELEASE_DIR"
RELEASE_XPI="verstak-firefox-${VERSION}.xpi" RELEASE_XPI="verstak-firefox-${VERSION}.xpi"
cp "$SIGNED_XPI" "$RELEASE_DIR/$RELEASE_XPI" cp "$SIGNED_XPI" "$RELEASE_DIR/$RELEASE_XPI"
cat > "$RELEASE_DIR/updates.json" <<EOF node scripts/firefox-github-release.js write-updates \
{ "$ADDON_ID" "$VERSION" "$RELEASE_XPI" "$RELEASE_DIR/updates.json"
"addons": {
"${ADDON_ID}": {
"updates": [
{
"version": "${VERSION}",
"update_link": "${UPDATE_BASE_URL}/${RELEASE_XPI}"
}
]
}
}
}
EOF
echo "Firefox release artifacts:" echo "Firefox release artifacts:"
echo "$RELEASE_DIR/$RELEASE_XPI" echo "$RELEASE_DIR/$RELEASE_XPI"
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
const assert = require('assert');
const fs = require('fs');
const release = require('./firefox-github-release');
const packageManifest = require('../package.json');
const chromiumManifest = require('../chromium/manifest.json');
const firefoxManifest = require('../firefox/manifest.json');
const version = '2.0.6';
const asset = 'verstak-firefox-2.0.6.xpi';
assert.equal(packageManifest.version, version);
assert.equal(chromiumManifest.version, version);
assert.equal(firefoxManifest.version, version);
assert.equal(release.releaseTag(version), 'v2.0.6');
assert.equal(
release.releaseAssetURL(version, asset),
'https://github.com/mirivlad/verstak-browser-extension/releases/download/v2.0.6/verstak-firefox-2.0.6.xpi',
);
assert.deepEqual(release.updateManifest('verstak-bridge@verstak.app', version, asset), {
addons: {
'verstak-bridge@verstak.app': {
updates: [{
version: '2.0.6',
update_link: 'https://github.com/mirivlad/verstak-browser-extension/releases/download/v2.0.6/verstak-firefox-2.0.6.xpi',
}],
},
},
});
assert.equal(firefoxManifest.homepage_url, 'https://github.com/mirivlad/verstak-browser-extension');
assert.equal(
firefoxManifest.browser_specific_settings.gecko.update_url,
'https://github.com/mirivlad/verstak-browser-extension/releases/latest/download/updates.json',
);
const publisher = fs.readFileSync('scripts/publish-firefox-github-release.sh', 'utf8');
assert.match(publisher, /auth status/);
assert.match(publisher, /branch --show-current/);
assert.match(publisher, /tag -a/);
assert.match(publisher, /push origin/);
assert.match(publisher, /release create/);
assert.match(publisher, /release upload/);
assert.match(publisher, /--clobber/);
assert.match(publisher, /--latest/);
assert.match(publisher, /--notes-file/);
assert.match(publisher, /--generate-notes/);
assert.match(publisher, /--notes-start-tag/);
const genericPublisher = fs.readFileSync('scripts/publish-github-release.sh', 'utf8');
assert.match(genericPublisher, /publish-firefox-github-release\.sh/);
console.log('Firefox GitHub release metadata tests passed');
+1 -1
View File
@@ -29,7 +29,7 @@ assert.ok(Object.values(ru).every((value) => typeof value === 'string'));
const tEn = i18n.createTranslator({ en, ru }, 'en'); const tEn = i18n.createTranslator({ en, ru }, 'en');
const tRu = i18n.createTranslator({ en, ru }, 'ru'); const tRu = i18n.createTranslator({ en, ru }, 'ru');
assert.strictEqual(tRu('status.queued'), 'В очереди до запуска Верстака'); assert.strictEqual(tRu('status.queued'), 'В очереди до запуска Верстака');
assert.strictEqual(tEn('error.value', { error: 'offline' }), 'Error: offline'); assert.strictEqual(tEn('error.sendCapture'), 'Could not send the capture. Please try again.');
assert.strictEqual(tRu('missing', null, 'Fallback'), 'Fallback'); assert.strictEqual(tRu('missing', null, 'Fallback'), 'Fallback');
assert.strictEqual(tRu('missing.key'), 'missing.key'); assert.strictEqual(tRu('missing.key'), 'missing.key');
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env node
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const root = path.resolve(__dirname, '..');
const source = fs.readFileSync(path.join(root, 'shared', 'icons', 'verstak.svg'), 'utf8');
assert.match(source, /fill="#1c2f4a"/i, 'icon source must define the Verstak navy');
assert.match(source, /fill="#72d2b3"/i, 'icon source must define the Verstak mint');
function paeth(left, up, upperLeft) {
const estimate = left + up - upperLeft;
const leftDistance = Math.abs(estimate - left);
const upDistance = Math.abs(estimate - up);
const upperLeftDistance = Math.abs(estimate - upperLeft);
if (leftDistance <= upDistance && leftDistance <= upperLeftDistance) return left;
if (upDistance <= upperLeftDistance) return up;
return upperLeft;
}
function pngColors(file) {
const png = fs.readFileSync(file);
assert.deepStrictEqual(png.subarray(0, 8), Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), `${file} must be PNG`);
let offset = 8;
let width;
let height;
let bitDepth;
let colorType;
let palette;
const idat = [];
while (offset < png.length) {
const length = png.readUInt32BE(offset);
const type = png.toString('ascii', offset + 4, offset + 8);
const data = png.subarray(offset + 8, offset + 8 + length);
offset += length + 12;
if (type === 'IHDR') {
width = data.readUInt32BE(0);
height = data.readUInt32BE(4);
bitDepth = data[8];
colorType = data[9];
} else if (type === 'PLTE') {
palette = data;
} else if (type === 'IDAT') {
idat.push(data);
}
}
assert.equal(bitDepth, 8, `${file} must use 8-bit samples`);
assert.ok([2, 3, 6].includes(colorType), `${file} must use RGB, palette, or RGBA pixels`);
const bytesPerPixel = colorType === 6 ? 4 : colorType === 2 ? 3 : 1;
const rowSize = width * bytesPerPixel;
const decoded = zlib.inflateSync(Buffer.concat(idat));
const colors = new Set();
let readOffset = 0;
let previous = Buffer.alloc(rowSize);
for (let y = 0; y < height; y += 1) {
const filter = decoded[readOffset++];
const encoded = decoded.subarray(readOffset, readOffset + rowSize);
readOffset += rowSize;
const row = Buffer.alloc(rowSize);
for (let x = 0; x < rowSize; x += 1) {
const left = x >= bytesPerPixel ? row[x - bytesPerPixel] : 0;
const up = previous[x];
const upperLeft = x >= bytesPerPixel ? previous[x - bytesPerPixel] : 0;
if (filter === 0) row[x] = encoded[x];
else if (filter === 1) row[x] = (encoded[x] + left) & 255;
else if (filter === 2) row[x] = (encoded[x] + up) & 255;
else if (filter === 3) row[x] = (encoded[x] + Math.floor((left + up) / 2)) & 255;
else if (filter === 4) row[x] = (encoded[x] + paeth(left, up, upperLeft)) & 255;
else assert.fail(`${file} has unsupported PNG filter ${filter}`);
}
for (let x = 0; x < width; x += 1) {
if (colorType === 3) {
const paletteOffset = row[x] * 3;
colors.add(palette.subarray(paletteOffset, paletteOffset + 3).toString('hex'));
} else {
const pixelOffset = x * bytesPerPixel;
colors.add(row.subarray(pixelOffset, pixelOffset + 3).toString('hex'));
}
}
previous = row;
}
return { width, height, colors };
}
for (const [name, size, needsMint] of [
['icon16.png', 16, false],
['icon48.png', 48, true],
['icon128.png', 128, true],
]) {
const icon = pngColors(path.join(root, 'shared', 'icons', name));
assert.equal(icon.width, size, `${name} width`);
assert.equal(icon.height, size, `${name} height`);
assert.ok(icon.colors.has('1c2f4a'), `${name} must contain the Verstak navy`);
if (needsMint) assert.ok(icon.colors.has('72d2b3'), `${name} must contain the Verstak mint`);
}
console.log('browser extension icon tests passed');
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env node
const assert = require('assert');
const chromium = require('../chromium/manifest.json');
const firefox = require('../firefox/manifest.json');
assert.equal(chromium.version, firefox.version, 'browser packages must use the same version');
assert.equal(
firefox.permissions.includes('windows'),
false,
'Firefox does not accept the Chromium-only windows permission'
);
assert.equal(chromium.permissions.includes('windows'), true, 'Chromium still needs the windows permission');
console.log('browser manifest compatibility tests passed');
+10
View File
@@ -0,0 +1,10 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const css = fs.readFileSync(path.join(__dirname, '..', 'shared', 'popup', 'popup.css'), 'utf8');
assert.match(css, /select\s*\{[^}]*appearance:\s*none/, 'popup select must hide the native arrow');
assert.match(css, /select option\s*\{[^}]*background/, 'popup options must use the extension surface');
console.log('browser extension popup select style test passed');
+31 -2
View File
@@ -65,6 +65,8 @@ const elements = {};
}); });
let savedSettings = null; let savedSettings = null;
let nextRequestError = null;
const technicalWarnings = [];
const initialState = { const initialState = {
settings: { settings: {
receiverUrl: 'http://127.0.0.1:47731/api/browser-inbox/v1/captures', receiverUrl: 'http://127.0.0.1:47731/api/browser-inbox/v1/captures',
@@ -80,6 +82,11 @@ const browser = {
runtime: { runtime: {
getURL(relativePath) { return `extension://${relativePath}`; }, getURL(relativePath) { return `extension://${relativePath}`; },
sendMessage(message) { sendMessage(message) {
if (nextRequestError) {
const error = nextRequestError;
nextRequestError = null;
return Promise.reject(new Error(error));
}
if (message.action === 'getState') return Promise.resolve(initialState); if (message.action === 'getState') return Promise.resolve(initialState);
if (message.action === 'saveSettings') { if (message.action === 'saveSettings') {
savedSettings = message.settings; savedSettings = message.settings;
@@ -107,10 +114,24 @@ function fetchCatalog(url) {
} }
const i18nPath = path.join(__dirname, '..', 'shared', 'i18n.js'); const i18nPath = path.join(__dirname, '..', 'shared', 'i18n.js');
const popupPath = path.join(__dirname, '..', 'shared', 'popup', 'popup.js'); const popupPath = path.join(__dirname, '..', 'shared', 'popup', 'popup.js');
const context = vm.createContext({ browser, console, document, Promise, btoa, fetch: fetchCatalog, navigator: { language: 'en-US' } }); const popupSource = fs.readFileSync(popupPath, 'utf8');
assert.equal(/setStatus\(\s*(?:error\b|err\b|String\()/.test(popupSource), false);
const context = vm.createContext({
browser,
console: {
error: console.error,
log: console.log,
warn(...args) { technicalWarnings.push(args.map(String).join(' ')); },
},
document,
Promise,
btoa,
fetch: fetchCatalog,
navigator: { language: 'en-US' },
});
context.globalThis = context; context.globalThis = context;
vm.runInContext(fs.readFileSync(i18nPath, 'utf8'), context, { filename: i18nPath }); vm.runInContext(fs.readFileSync(i18nPath, 'utf8'), context, { filename: i18nPath });
vm.runInContext(fs.readFileSync(popupPath, 'utf8'), context, { filename: popupPath }); vm.runInContext(popupSource, context, { filename: popupPath });
async function flush() { async function flush() {
for (let i = 0; i < 16; i += 1) await Promise.resolve(); for (let i = 0; i < 16; i += 1) await Promise.resolve();
@@ -156,6 +177,14 @@ async function flush() {
assert.strictEqual(savedSettings.language, 'en'); assert.strictEqual(savedSettings.language, 'en');
assert.strictEqual(savedSettings.passiveActivityEnabled, true); assert.strictEqual(savedSettings.passiveActivityEnabled, true);
assert.deepStrictEqual(Array.from(savedSettings.passiveActivityExcludedDomains), ['youtube.com', 'x.com']); assert.deepStrictEqual(Array.from(savedSettings.passiveActivityExcludedDomains), ['youtube.com', 'x.com']);
nextRequestError = '[plugin:verstak.browser-inbox] captures.create failed: receiver unavailable';
elements['capture-page'].click();
await flush();
assert.strictEqual(elements.status.textContent, 'Could not send the capture. Please try again.');
assert.equal(elements.status.textContent.includes('[plugin:'), false);
assert.ok(technicalWarnings.some((message) => message.includes('captures.create failed')));
console.log('browser extension popup localization/settings tests passed'); console.log('browser extension popup localization/settings tests passed');
})().catch((error) => { })().catch((error) => {
console.error(error); console.error(error);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/hostname-normalization-v1.json", "$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/hostname-normalization-v1.json",
"title": "Verstak canonical hostname normalization v1 test vectors", "title": "Verstak canonical hostname normalization v1 test vectors",
"description": "Canonical hostnames are lowercase ASCII A-labels without a port or trailing DNS dot. Bare hostnames accept DNS names, IPv4, bracketed IPv6, localhost, and internal single-label names. URL inputs accept only HTTP(S). Invalid or excessively long input normalizes to an empty string.", "description": "Canonical hostnames are lowercase ASCII A-labels without a port or trailing DNS dot. Bare hostnames accept DNS names, IPv4, bracketed IPv6, localhost, and internal single-label names. URL inputs accept only HTTP(S). Invalid or excessively long input normalizes to an empty string.",
"version": 1, "version": 1,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<rect width="128" height="128" rx="26" fill="#1c2f4a"/>
<path d="M28 34h72v18H28zm0 31h72v29H28z" fill="#72d2b3"/>
<path d="M41 52v13m23-13v13m23-13v13" stroke="#1c2f4a" stroke-width="8"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

+4 -1
View File
@@ -30,7 +30,10 @@
"error.chooseFile": "Choose a file first", "error.chooseFile": "Choose a file first",
"error.fileTooLarge": "File is too large for browser capture", "error.fileTooLarge": "File is too large for browser capture",
"error.invalidReceiverUrl": "Receiver URL must start with http:// or https://", "error.invalidReceiverUrl": "Receiver URL must start with http:// or https://",
"error.value": "Error: {error}", "error.loadState": "Could not load the extension state. Please try again.",
"error.saveSettings": "Could not save settings. Please try again.",
"error.sendCapture": "Could not send the capture. Please try again.",
"error.readFile": "Could not read the file. Choose it again.",
"context.sendPage": "Send page to Verstak", "context.sendPage": "Send page to Verstak",
"context.sendSelection": "Send selection to Verstak", "context.sendSelection": "Send selection to Verstak",
"context.sendLink": "Send link to Verstak" "context.sendLink": "Send link to Verstak"
+4 -1
View File
@@ -30,7 +30,10 @@
"error.chooseFile": "Сначала выберите файл", "error.chooseFile": "Сначала выберите файл",
"error.fileTooLarge": "Файл слишком велик для отправки из браузера", "error.fileTooLarge": "Файл слишком велик для отправки из браузера",
"error.invalidReceiverUrl": "URL приёмника должен начинаться с http:// или https://", "error.invalidReceiverUrl": "URL приёмника должен начинаться с http:// или https://",
"error.value": "Ошибка: {error}", "error.loadState": "Не удалось загрузить данные расширения. Повторите попытку.",
"error.saveSettings": "Не удалось сохранить настройки. Повторите попытку.",
"error.sendCapture": "Не удалось отправить материал. Повторите попытку.",
"error.readFile": "Не удалось прочитать файл. Выберите его ещё раз.",
"context.sendPage": "Отправить страницу в Верстак", "context.sendPage": "Отправить страницу в Верстак",
"context.sendSelection": "Отправить выделение в Верстак", "context.sendSelection": "Отправить выделение в Верстак",
"context.sendLink": "Отправить ссылку в Верстак" "context.sendLink": "Отправить ссылку в Верстак"
+11 -1
View File
@@ -145,7 +145,17 @@ select {
margin-top: 5px; margin-top: 5px;
border: 1px solid #374151; border: 1px solid #374151;
border-radius: 4px; border-radius: 4px;
padding: 7px 8px; padding: 7px 30px 7px 8px;
appearance: none;
background-color: #0f172a;
background-image: linear-gradient(45deg, transparent 50%, #94a3b8 50%), linear-gradient(135deg, #94a3b8 50%, transparent 50%);
background-position: calc(100% - 16px) 50%, calc(100% - 11px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
color: #e5e7eb;
}
select option {
background: #0f172a; background: #0f172a;
color: #e5e7eb; color: #e5e7eb;
} }
+9 -4
View File
@@ -118,6 +118,11 @@
statusEl.textContent = text; statusEl.textContent = text;
} }
function reportError(key, fallback, error) {
console.warn('[verstak.popup] request failed:', error);
setStatus(t(key, null, fallback));
}
function request(message) { function request(message) {
return Promise.resolve(ext.runtime.sendMessage(message)).then(function (result) { return Promise.resolve(ext.runtime.sendMessage(message)).then(function (result) {
if (result && result.error) throw new Error(result.error); if (result && result.error) throw new Error(result.error);
@@ -148,7 +153,7 @@
applyLocale(state.settings && state.settings.language || 'system'); applyLocale(state.settings && state.settings.language || 'system');
return state; return state;
}).catch(function (error) { }).catch(function (error) {
setStatus(error && error.message ? error.message : String(error)); reportError('error.loadState', 'Could not load the extension state. Please try again.', error);
}); });
} }
@@ -174,7 +179,7 @@
if (successMessage) setStatus(t('status.saved', null, 'Saved')); if (successMessage) setStatus(t('status.saved', null, 'Saved'));
return state; return state;
}).catch(function (error) { }).catch(function (error) {
setStatus(error && error.message ? error.message : String(error)); reportError('error.saveSettings', 'Could not save settings. Please try again.', error);
}); });
} }
@@ -188,7 +193,7 @@
setStatus(t('status.done', null, 'Done')); setStatus(t('status.done', null, 'Done'));
} }
}).catch(function (error) { }).catch(function (error) {
setStatus(error && error.message ? error.message : String(error)); reportError('error.sendCapture', 'Could not send the capture. Please try again.', error);
}); });
} }
@@ -218,7 +223,7 @@
fileDataBase64: arrayBufferToBase64(results[0]) fileDataBase64: arrayBufferToBase64(results[0])
}); });
}).catch(function (error) { }).catch(function (error) {
setStatus(error && error.message ? error.message : String(error)); reportError('error.readFile', 'Could not read the file. Choose it again.', error);
}); });
}); });