Compare commits

..

No commits in common. "f39550830e39516c0e4c8b4c151ed687380437c8" and "d8bd1d18284e25ae14ae1c1b11f122f22aa413f5" have entirely different histories.

23 changed files with 31 additions and 944 deletions

17
LICENSE
View File

@ -1,17 +0,0 @@
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

View File

@ -1,4 +1,4 @@
# Verstak Browser Extension
# verstak-browser-extension
Verstak Browser Extension captures pages, selected text, links, and selected
files and sends them to a local Verstak browser inbox receiver.
@ -7,8 +7,6 @@ The extension does not know Notes, Files, Activity, or Journal internals. It
only sends capture events through the public local receiver protocol. If the
receiver is offline, captures stay in the extension pending queue.
> **Alpha software.** Use with a matching Verstak Desktop alpha release.
## Build
```bash
@ -22,23 +20,6 @@ Build output:
- `dist/chromium`
- `dist/firefox`
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 Release
Firefox signing uses `web-ext` and AMO credentials from an env file. The script
@ -57,16 +38,6 @@ Release output:
The XPI is signed as an unlisted/self-distributed Firefox extension. 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
1. Start Verstak desktop with the `verstak.browser-inbox` plugin installed.
@ -138,8 +109,3 @@ Expected success response:
```json
{ "status": "accepted", "captureId": "uuid-or-generated-id" }
```
## License
Copyright © 2026 Verstak contributors. Licensed under
[GNU AGPLv3 or later](LICENSE).

View File

@ -5,7 +5,7 @@
"description": "Send pages, selections, links, and files to the local Verstak browser inbox.",
"author": "Verstak",
"homepage_url": "https://git.mirv.top/verstak/verstak-browser-extension",
"permissions": ["alarms", "contextMenus", "idle", "storage", "tabs", "windows"],
"permissions": ["contextMenus", "storage", "tabs"],
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
"background": {
"service_worker": "background.js"

View File

@ -15,9 +15,9 @@
}
}
},
"permissions": ["alarms", "contextMenus", "idle", "storage", "tabs", "windows", "http://127.0.0.1/*", "http://localhost/*"],
"permissions": ["contextMenus", "storage", "tabs", "http://127.0.0.1/*", "http://localhost/*"],
"background": {
"scripts": ["hostname.js", "activity-tracker.js", "protocol.js", "api.js", "queue.js", "i18n.js", "background.js"]
"scripts": ["protocol.js", "api.js", "queue.js", "i18n.js", "background.js"]
},
"browser_action": {
"default_popup": "popup/popup.html",

View File

@ -3,13 +3,11 @@
"version": "2.0.2",
"private": true,
"description": "Verstak browser capture extension for Chromium and Firefox",
"license": "AGPL-3.0-or-later",
"scripts": {
"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-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",
"sign:firefox": "./scripts/sign-firefox-xpi.sh",
"release:firefox": "./scripts/release-firefox-xpi.sh",
"release:package": "./scripts/package-release.sh"
"release:firefox": "./scripts/release-firefox-xpi.sh"
},
"devDependencies": {
"web-ext": "^8.3.0"

View File

@ -51,8 +51,6 @@ const chromiumDist = path.join(dist, 'chromium');
mkdir(chromiumDist);
copy(path.join(root, 'chromium', 'manifest.json'), path.join(chromiumDist, 'manifest.json'));
concat([
path.join(shared, 'hostname.js'),
path.join(shared, 'activity-tracker.js'),
path.join(shared, 'protocol.js'),
path.join(shared, 'api.js'),
path.join(shared, 'queue.js'),
@ -66,7 +64,7 @@ copyLocalization(chromiumDist);
const firefoxDist = path.join(dist, 'firefox');
mkdir(firefoxDist);
copy(path.join(root, 'firefox', 'manifest.json'), path.join(firefoxDist, 'manifest.json'));
for (const name of ['hostname.js', 'activity-tracker.js', 'protocol.js', 'api.js', 'queue.js', 'i18n.js', 'background.js']) {
for (const name of ['protocol.js', 'api.js', 'queue.js', 'i18n.js', 'background.js']) {
copy(path.join(shared, name), path.join(firefoxDist, name));
}
copyPopup(firefoxDist);

View File

@ -1,28 +0,0 @@
#!/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"

View File

@ -1,70 +0,0 @@
#!/usr/bin/env node
const assert = require('assert');
const hostname = require('../shared/hostname');
const trackerApi = require('../shared/activity-tracker');
async function main() {
const storage = trackerApi.createMemoryActivityStorage();
const sent = [];
let failSends = true;
const tracker = new trackerApi.DomainActivityTracker(storage, async (batch) => {
sent.push(JSON.parse(JSON.stringify(batch)));
if (failSends) throw new Error('offline');
return { status: 'accepted', batchId: batch.batchId };
});
await tracker.initialize();
await tracker.setEnabled(true);
await tracker.setActiveHostname('example.com', true, 0);
await tracker.checkpoint(600000);
await tracker.flush(600000);
let state = await tracker.getState();
assert.equal(state.pendingBatches.length, 1);
assert.equal(state.pendingBatches[0].entries[0].durationSeconds, 600);
const immutableFirstBatch = JSON.stringify(sent[0]);
await tracker.setActiveHostname('example.com', true, 900000);
await tracker.flush(900000);
state = await tracker.getState();
assert.equal(state.pendingBatches.length, 2, 'new activity must not mutate sent batch A');
assert.equal(state.pendingBatches[1].entries[0].durationSeconds, 300);
failSends = false;
await tracker.retryPending();
state = await tracker.getState();
assert.equal(state.pendingBatches.length, 0);
assert.ok(state.acknowledgedIds.length >= 2);
assert.equal(JSON.stringify(sent[1]), immutableFirstBatch, 'retry must resend an immutable batch A payload');
const clockTracker = new trackerApi.DomainActivityTracker(trackerApi.createMemoryActivityStorage(), async () => ({ status: 'accepted' }));
await clockTracker.initialize();
await clockTracker.setEnabled(true);
await clockTracker.setActiveHostname('example.com', true, 0);
await clockTracker.checkpoint(8 * 60 * 60 * 1000);
state = await clockTracker.getState();
assert.equal(state.activeAccumulator['example.com'].durationMs, 0, 'sleep-sized gap must be discarded');
await clockTracker.checkpoint(8 * 60 * 60 * 1000 + 5 * 60 * 1000);
state = await clockTracker.getState();
assert.equal(state.activeAccumulator['example.com'].durationMs, 300000);
await clockTracker.checkpoint(1000000);
state = await clockTracker.getState();
assert.equal(state.activeAccumulator['example.com'].durationMs, 0, 'clock rollback must reset the ambiguous interval');
assert.equal(trackerApi.isExcludedHostname('video.youtube.com', ['youtube.com']), true);
assert.equal(trackerApi.isExcludedHostname('youtube.com.evil.test', ['youtube.com']), false);
assert.equal(trackerApi.isExcludedHostname(hostname.normalizeHostnameV1('пример.рф'), ['пример.рф']), true);
await tracker.setEnabled(false);
state = await tracker.getState();
assert.deepEqual(state.activeAccumulator, {});
assert.deepEqual(state.pendingBatches, []);
assert.equal(JSON.stringify(state).includes('https://'), false);
console.log('browser activity tracker tests passed');
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

View File

@ -49,13 +49,7 @@ const browser = {
onMessage: { addListener(listener) { messageListener = listener; } },
},
i18n: { getUILanguage() { return 'en-US'; } },
tabs: {
query() { return Promise.resolve([{ id: 7, windowId: 1, url: 'https://example.com/private-path', title: 'Private title' }]); },
get() { return Promise.resolve({ id: 7, windowId: 1, url: 'https://example.com/private-path', title: 'Private title' }); },
onActivated: { addListener() {} },
onUpdated: { addListener() {} },
onRemoved: { addListener() {} },
},
tabs: { query() { return Promise.resolve([]); } },
};
function fetchCatalog(url) {
@ -75,7 +69,7 @@ const context = vm.createContext({
clearTimeout,
});
context.globalThis = context;
for (const file of ['hostname.js', 'activity-tracker.js', 'protocol.js', 'api.js', 'queue.js', 'i18n.js', 'background.js']) {
for (const file of ['protocol.js', 'api.js', 'queue.js', 'i18n.js', 'background.js']) {
vm.runInContext(fs.readFileSync(path.join(root, 'shared', file), 'utf8'), context, { filename: file });
}
@ -98,22 +92,6 @@ function sendMessage(message) {
'Отправить ссылку в Верстак',
]);
const trackingState = await sendMessage({
type: 'verstak.capture',
action: 'saveSettings',
settings: {
receiverUrl: state.settings.receiverUrl,
receiverToken: state.settings.receiverToken,
language: 'en',
passiveActivityEnabled: true,
passiveActivityExcludedDomains: [],
},
});
assert.strictEqual(trackingState.settings.passiveActivityEnabled, true);
assert.ok(trackingState.activityState.activeAccumulator['example.com']);
assert.equal(JSON.stringify(trackingState.activityState).includes('private-path'), false);
assert.equal(JSON.stringify(trackingState.activityState).includes('Private title'), false);
const nextState = await sendMessage({
type: 'verstak.capture',
action: 'saveSettings',

View File

@ -1,18 +0,0 @@
#!/usr/bin/env node
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const hostname = require('../shared/hostname');
const vectors = JSON.parse(fs.readFileSync(path.join(__dirname, '../shared/hostname-normalization-v1.json'), 'utf8'));
for (const vector of vectors.bare) {
assert.equal(hostname.normalizeHostnameV1(vector.input), vector.output, vector.input);
}
for (const vector of vectors.url) {
assert.equal(hostname.normalizeURLHostnameV1(vector.input), vector.output, vector.input);
}
console.log('browser hostname normalization tests passed');

View File

@ -35,12 +35,10 @@ const elements = {};
'receiver-token-input',
'file-input',
'pending-count',
'activity-pending-count',
'status-dot',
'subtitle',
'receiver-label',
'pending-label',
'activity-pending-label',
'url-label',
'capture-page',
'capture-file',
@ -55,11 +53,6 @@ const elements = {};
'language-ru-option',
'save-settings',
'context-menu-hint',
'passive-activity-enabled',
'passive-activity-label',
'passive-activity-disclosure',
'passive-activity-exclusions-label',
'passive-activity-exclusions',
].forEach((id) => {
elements[id] = new Element();
});
@ -70,8 +63,6 @@ const initialState = {
receiverUrl: 'http://127.0.0.1:47731/api/browser-inbox/v1/captures',
receiverToken: 'persisted-token',
language: 'system',
passiveActivityEnabled: false,
passiveActivityExcludedDomains: ['youtube.com'],
},
pendingCount: 0,
status: {},
@ -121,8 +112,6 @@ async function flush() {
assert.strictEqual(elements['receiver-input'].value, initialState.settings.receiverUrl);
assert.strictEqual(elements['receiver-token-input'].value, initialState.settings.receiverToken);
assert.strictEqual(elements['language-select'].value, 'system');
assert.strictEqual(elements['passive-activity-enabled'].checked, false);
assert.strictEqual(elements['passive-activity-exclusions'].value, 'youtube.com');
assert.strictEqual(elements['capture-page'].textContent, 'Отправить страницу');
assert.strictEqual(elements['receiver-state'].textContent, 'Неизвестно');
assert.strictEqual(document.documentElement.lang, 'ru');
@ -140,13 +129,9 @@ async function flush() {
assert.strictEqual(savedSettings.language, 'en');
assert.strictEqual(savedSettings.receiverUrl, initialState.settings.receiverUrl);
assert.strictEqual(savedSettings.receiverToken, initialState.settings.receiverToken);
assert.strictEqual(savedSettings.passiveActivityEnabled, false);
assert.deepStrictEqual(Array.from(savedSettings.passiveActivityExcludedDomains), ['youtube.com']);
elements['receiver-input'].value = 'http://127.0.0.1:47731/api/browser-inbox/v1/captures';
elements['receiver-token-input'].value = 'new-token';
elements['passive-activity-enabled'].checked = true;
elements['passive-activity-exclusions'].value = 'youtube.com\nx.com';
elements['save-settings'].click();
await flush();
@ -154,8 +139,6 @@ async function flush() {
assert.strictEqual(savedSettings.receiverUrl, 'http://127.0.0.1:47731/api/browser-inbox/v1/captures');
assert.strictEqual(savedSettings.receiverToken, 'new-token');
assert.strictEqual(savedSettings.language, 'en');
assert.strictEqual(savedSettings.passiveActivityEnabled, true);
assert.deepStrictEqual(Array.from(savedSettings.passiveActivityExcludedDomains), ['youtube.com', 'x.com']);
console.log('browser extension popup localization/settings tests passed');
})().catch((error) => {
console.error(error);

View File

@ -1,7 +1,6 @@
#!/usr/bin/env node
const assert = require('assert');
require('../shared/hostname');
const protocol = require('../shared/protocol');
require('../shared/api');
const queueApi = require('../shared/queue');
@ -52,20 +51,6 @@ const fetchOk = (url, options) => {
return Promise.resolve({ status: 202, json: () => Promise.resolve({ status: 'accepted' }) });
};
const activityBatch = {
schemaVersion: 1,
batchId: 'activity-batch-id',
createdAt: '2026-07-12T10:05:00.000Z',
source: 'verstak-browser-extension',
entries: [{
hostname: 'example.com',
startedAt: '2026-07-12T10:00:00.000Z',
endedAt: '2026-07-12T10:05:00.000Z',
durationSeconds: 300,
url: 'https://must-not-be-sent.example'
}]
};
globalThis.VerstakBrowser.sendCapture('http://127.0.0.1:47731/api/browser-inbox/v1/captures', 'token', page, fetchOk)
.then((result) => {
assert.equal(result.status, 'accepted');
@ -73,17 +58,6 @@ globalThis.VerstakBrowser.sendCapture('http://127.0.0.1:47731/api/browser-inbox/
assert.equal(request.options.headers['X-Verstak-Receiver-Token'], 'token');
assert.equal(JSON.parse(request.options.body).captureId, 'test-capture-id');
})
.then(() => {
return globalThis.VerstakBrowser.sendActivityBatch('http://127.0.0.1:47731/api/browser-inbox/v1/captures', 'token', activityBatch, fetchOk)
.then((result) => {
assert.equal(result.status, 'accepted');
assert.equal(request.url, 'http://127.0.0.1:47731/api/browser-activity/v1/batches');
const body = JSON.parse(request.options.body);
assert.equal(body.batchId, 'activity-batch-id');
assert.equal(body.entries[0].hostname, 'example.com');
assert.equal(Object.prototype.hasOwnProperty.call(body.entries[0], 'url'), false);
});
})
.then(() => {
const queue = new queueApi.CaptureQueue(queueApi.createMemoryStorage());
return queue.enqueue(page)

View File

@ -1,283 +0,0 @@
(function (root) {
'use strict';
var ACTIVITY_STATE_KEY = 'verstak.domainActivity.v1';
var MAX_CHECKPOINT_GAP_MS = 10 * 60 * 1000;
var ACK_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
var MAX_ACKNOWLEDGED_IDS = 1000;
function createMemoryActivityStorage(seed) {
var state = Object.assign({}, seed || {});
return {
get: function (key) {
return Promise.resolve(state[key]);
},
set: function (key, value) {
state[key] = value;
return Promise.resolve();
}
};
}
function browserActivityStorageAdapter(browserApi) {
var storage = browserApi && browserApi.storage && browserApi.storage.local;
if (!storage) return createMemoryActivityStorage();
return {
get: function (key) {
return storage.get(key).then(function (result) { return result && result[key]; });
},
set: function (key, value) {
var patch = {};
patch[key] = value;
return storage.set(patch);
}
};
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function isFiniteNumber(value) {
return typeof value === 'number' && Number.isFinite(value);
}
function initialState() {
return {
activeAccumulator: {},
pendingBatches: [],
acknowledgedIds: []
};
}
function normalizedState(value) {
var state = value && typeof value === 'object' ? value : {};
return {
activeAccumulator: state.activeAccumulator && typeof state.activeAccumulator === 'object' ? state.activeAccumulator : {},
pendingBatches: Array.isArray(state.pendingBatches) ? state.pendingBatches : [],
acknowledgedIds: Array.isArray(state.acknowledgedIds) ? state.acknowledgedIds : []
};
}
function batchId(now) {
var cryptoObj = root.crypto;
if (cryptoObj && typeof cryptoObj.randomUUID === 'function') return cryptoObj.randomUUID();
return 'activity-' + String(now) + '-' + Math.random().toString(36).slice(2);
}
function toISOString(time) {
return new Date(time).toISOString();
}
function isExcludedHostname(hostname, excludedDomains) {
var normalizer = root.VerstakBrowser && root.VerstakBrowser.normalizeHostnameV1;
var canonicalHostname = typeof normalizer === 'function' ? normalizer(hostname) : '';
if (!canonicalHostname) return true;
return (Array.isArray(excludedDomains) ? excludedDomains : []).some(function (value) {
var excluded = typeof normalizer === 'function' ? normalizer(value) : '';
return excluded && (canonicalHostname === excluded || canonicalHostname.slice(-(excluded.length + 1)) === '.' + excluded);
});
}
function DomainActivityTracker(storage, sender, options) {
options = options || {};
this.storage = storage || createMemoryActivityStorage();
this.sender = typeof sender === 'function' ? sender : function () { return Promise.reject(new Error('activity sender unavailable')); };
this.maxCheckpointGapMs = isFiniteNumber(options.maxCheckpointGapMs) ? options.maxCheckpointGapMs : MAX_CHECKPOINT_GAP_MS;
this.now = typeof options.now === 'function' ? options.now : function () { return Date.now(); };
this.state = initialState();
this.enabled = false;
this.activeHostname = '';
this.serial = Promise.resolve();
}
DomainActivityTracker.prototype.enqueue = function (operation) {
var next = this.serial.then(operation, operation);
this.serial = next.catch(function () {});
return next;
};
DomainActivityTracker.prototype.initialize = function () {
var self = this;
return this.enqueue(function () {
return self.storage.get(ACTIVITY_STATE_KEY).then(function (value) {
self.state = normalizedState(value);
self.pruneAcknowledged(self.now());
return self.persist().then(function () { return self.getStateUnsafe(); });
});
});
};
DomainActivityTracker.prototype.getState = function () {
var self = this;
return this.enqueue(function () { return self.getStateUnsafe(); });
};
DomainActivityTracker.prototype.getStateUnsafe = function () {
return clone(this.state);
};
DomainActivityTracker.prototype.persist = function () {
return this.storage.set(ACTIVITY_STATE_KEY, clone(this.state));
};
DomainActivityTracker.prototype.setEnabled = function (enabled) {
var self = this;
return this.enqueue(function () {
self.enabled = enabled === true;
if (!self.enabled) {
self.activeHostname = '';
self.state.activeAccumulator = {};
self.state.pendingBatches = [];
}
return self.persist().then(function () { return self.getStateUnsafe(); });
});
};
DomainActivityTracker.prototype.setActiveHostname = function (hostname, active, observedAt) {
var self = this;
var now = isFiniteNumber(observedAt) ? observedAt : this.now();
return this.enqueue(function () {
if (!self.enabled || !active || !hostname) {
self.checkpointUnsafe(now);
self.activeHostname = '';
return self.persist().then(function () { return self.getStateUnsafe(); });
}
self.checkpointUnsafe(now);
self.activeHostname = hostname;
self.ensureAccumulator(hostname, now);
return self.persist().then(function () { return self.getStateUnsafe(); });
});
};
DomainActivityTracker.prototype.checkpoint = function (observedAt) {
var self = this;
var now = isFiniteNumber(observedAt) ? observedAt : this.now();
return this.enqueue(function () {
self.checkpointUnsafe(now);
return self.persist().then(function () { return self.getStateUnsafe(); });
});
};
DomainActivityTracker.prototype.ensureAccumulator = function (hostname, now) {
if (!this.state.activeAccumulator[hostname]) {
this.state.activeAccumulator[hostname] = {
hostname: hostname,
startedAt: now,
lastCheckpointAt: now,
durationMs: 0
};
}
return this.state.activeAccumulator[hostname];
};
DomainActivityTracker.prototype.checkpointUnsafe = function (now) {
if (!this.enabled || !this.activeHostname) return;
var accumulator = this.ensureAccumulator(this.activeHostname, now);
var previous = accumulator.lastCheckpointAt;
var delta = now - previous;
if (!isFiniteNumber(previous) || delta < 0 || delta > this.maxCheckpointGapMs) {
accumulator.startedAt = now;
accumulator.lastCheckpointAt = now;
accumulator.durationMs = 0;
return;
}
if (delta === 0) return;
accumulator.durationMs += delta;
accumulator.lastCheckpointAt = now;
};
DomainActivityTracker.prototype.flush = function (observedAt) {
var self = this;
var now = isFiniteNumber(observedAt) ? observedAt : this.now();
return this.enqueue(function () {
if (!self.enabled) return self.getStateUnsafe();
self.checkpointUnsafe(now);
var entries = [];
Object.keys(self.state.activeAccumulator).forEach(function (hostname) {
var accumulator = self.state.activeAccumulator[hostname];
var durationSeconds = Math.floor(Number(accumulator.durationMs || 0) / 1000);
if (durationSeconds < 1) return;
entries.push({
hostname: accumulator.hostname,
startedAt: toISOString(accumulator.startedAt),
endedAt: toISOString(accumulator.lastCheckpointAt),
durationSeconds: durationSeconds
});
});
if (entries.length) {
self.state.pendingBatches.push({
schemaVersion: 1,
batchId: batchId(now),
createdAt: toISOString(now),
source: 'verstak-browser-extension',
entries: entries
});
self.state.activeAccumulator = {};
if (self.activeHostname) self.ensureAccumulator(self.activeHostname, now);
}
return self.persist().then(function () {
return self.deliverPendingUnsafe(now);
});
});
};
DomainActivityTracker.prototype.retryPending = function () {
var self = this;
return this.enqueue(function () {
if (!self.enabled) return self.getStateUnsafe();
return self.deliverPendingUnsafe(self.now());
});
};
DomainActivityTracker.prototype.deliverPendingUnsafe = function (now) {
var self = this;
var pending = this.state.pendingBatches.slice();
return pending.reduce(function (chain, batch) {
return chain.then(function () {
if (!self.hasPendingBatch(batch.batchId)) return undefined;
var immutableBatch = clone(batch);
return self.sender(immutableBatch).then(function (result) {
if (result && result.batchId && result.batchId !== batch.batchId) {
throw new Error('receiver acknowledged a different activity batch');
}
self.state.pendingBatches = self.state.pendingBatches.filter(function (item) { return item.batchId !== batch.batchId; });
self.state.acknowledgedIds.push({ id: batch.batchId, acknowledgedAt: toISOString(now) });
self.pruneAcknowledged(now);
return self.persist();
});
});
}, Promise.resolve()).then(function () {
return self.getStateUnsafe();
}).catch(function () {
return self.persist().then(function () { return self.getStateUnsafe(); });
});
};
DomainActivityTracker.prototype.hasPendingBatch = function (batchID) {
return this.state.pendingBatches.some(function (batch) { return batch.batchId === batchID; });
};
DomainActivityTracker.prototype.pruneAcknowledged = function (now) {
var cutoff = now - ACK_RETENTION_MS;
var seen = {};
this.state.acknowledgedIds = this.state.acknowledgedIds.filter(function (entry) {
var timestamp = Date.parse(entry && entry.acknowledgedAt);
if (!entry || !entry.id || !Number.isFinite(timestamp) || timestamp < cutoff || seen[entry.id]) return false;
seen[entry.id] = true;
return true;
}).slice(-MAX_ACKNOWLEDGED_IDS);
};
var api = {
ACTIVITY_STATE_KEY: ACTIVITY_STATE_KEY,
MAX_CHECKPOINT_GAP_MS: MAX_CHECKPOINT_GAP_MS,
createMemoryActivityStorage: createMemoryActivityStorage,
browserActivityStorageAdapter: browserActivityStorageAdapter,
DomainActivityTracker: DomainActivityTracker,
isExcludedHostname: isExcludedHostname
};
root.VerstakBrowser = Object.assign(root.VerstakBrowser || {}, api);
if (typeof module !== 'undefined') module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : this);

View File

@ -20,63 +20,7 @@
});
}
function activityReceiverURL(captureReceiverURL) {
var protocol = root.VerstakBrowser || {};
var receiverURL = String(captureReceiverURL || protocol.DEFAULT_RECEIVER_URL || '').trim();
var capturePath = '/api/browser-inbox/v1/captures';
if (receiverURL.slice(-capturePath.length) === capturePath) {
return receiverURL.slice(0, -capturePath.length) + '/api/browser-activity/v1/batches';
}
return protocol.DEFAULT_ACTIVITY_URL;
}
function safeActivityBatch(payload) {
if (!payload || payload.schemaVersion !== 1 || !payload.batchId || !payload.createdAt || !payload.source || !Array.isArray(payload.entries) || payload.entries.length === 0) {
throw new Error('invalid activity batch');
}
var entries = payload.entries.map(function (entry) {
if (!entry || !entry.hostname || !entry.startedAt || !entry.endedAt || !Number.isFinite(Number(entry.durationSeconds)) || Number(entry.durationSeconds) <= 0) {
throw new Error('invalid activity batch entry');
}
return {
hostname: String(entry.hostname),
startedAt: String(entry.startedAt),
endedAt: String(entry.endedAt),
durationSeconds: Math.floor(Number(entry.durationSeconds))
};
});
return {
schemaVersion: 1,
batchId: String(payload.batchId),
createdAt: String(payload.createdAt),
source: String(payload.source),
entries: entries
};
}
function sendActivityBatch(captureReceiverURL, token, payload, fetchImpl) {
var batch = safeActivityBatch(payload);
var fetchFn = fetchImpl || root.fetch;
if (typeof fetchFn !== 'function') return Promise.reject(new Error('fetch unavailable'));
return fetchFn(activityReceiverURL(captureReceiverURL), {
method: 'POST',
headers: Object.assign({
'Content-Type': 'application/json'
}, token ? { 'X-Verstak-Receiver-Token': token } : {}),
body: JSON.stringify(batch)
}).then(function (response) {
if (!response || response.status < 200 || response.status >= 300) {
throw new Error('receiver rejected activity batch: HTTP ' + (response && response.status));
}
return response.json ? response.json() : { status: 'accepted', batchId: batch.batchId };
});
}
var api = {
sendCapture: sendCapture,
sendActivityBatch: sendActivityBatch,
activityReceiverURL: activityReceiverURL
};
var api = { sendCapture: sendCapture };
root.VerstakBrowser = Object.assign(root.VerstakBrowser || {}, api);
if (typeof module !== 'undefined') module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : this);

View File

@ -4,34 +4,19 @@
var ext = typeof browser !== 'undefined' ? browser : chrome;
var protocol = globalThis.VerstakBrowser;
var queue = new protocol.CaptureQueue(protocol.browserStorageAdapter(ext));
var activityTracker = new protocol.DomainActivityTracker(
protocol.browserActivityStorageAdapter(ext),
function (batch) {
return getSettings().then(function (settings) {
return protocol.sendActivityBatch(settings.receiverUrl, settings.receiverToken, batch);
});
}
);
var i18n = globalThis.VerstakBrowserI18n;
var DEFAULT_SETTINGS = {
receiverUrl: protocol.DEFAULT_RECEIVER_URL,
receiverToken: '',
language: 'system',
passiveActivityEnabled: false,
passiveActivityExcludedDomains: []
language: 'system'
};
var STATUS_KEY = 'verstak.status';
var ACTIVITY_FLUSH_ALARM = 'verstak-domain-activity-flush';
var localeCatalogs = null;
var focusedWindowID = null;
var activeTabID = null;
function getSettings() {
return ext.storage.local.get('settings').then(function (result) {
var settings = Object.assign({}, DEFAULT_SETTINGS, result && result.settings || {});
settings.language = i18n.normalizePreference(settings.language);
settings.passiveActivityEnabled = settings.passiveActivityEnabled === true;
settings.passiveActivityExcludedDomains = normalizeExcludedDomains(settings.passiveActivityExcludedDomains);
return settings;
});
}
@ -39,23 +24,9 @@
function saveSettings(settings) {
settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
settings.language = i18n.normalizePreference(settings.language);
settings.passiveActivityEnabled = settings.passiveActivityEnabled === true;
settings.passiveActivityExcludedDomains = normalizeExcludedDomains(settings.passiveActivityExcludedDomains);
return ext.storage.local.set({ settings: settings });
}
function normalizeExcludedDomains(value) {
var values = Array.isArray(value) ? value : String(value || '').split(/[\n,]/);
var seen = {};
return values.map(function (item) {
return protocol.normalizeHostnameV1(String(item || '').trim());
}).filter(function (item) {
if (!item || seen[item]) return false;
seen[item] = true;
return true;
});
}
function loadLocaleCatalogs() {
if (localeCatalogs) return localeCatalogs;
localeCatalogs = i18n.loadCatalogs(function (locale) {
@ -97,58 +68,6 @@
});
}
function isFocusedWindow(windowID) {
return focusedWindowID == null || focusedWindowID === windowID;
}
function trackTab(tab, settings) {
if (!settings.passiveActivityEnabled || !tab || !isFocusedWindow(tab.windowId)) {
return activityTracker.setActiveHostname('', false);
}
activeTabID = tab.id == null ? activeTabID : tab.id;
var hostname = protocol.normalizeURLHostnameV1(tab.url || '');
if (!hostname || protocol.isExcludedHostname(hostname, settings.passiveActivityExcludedDomains)) {
return activityTracker.setActiveHostname('', false);
}
return activityTracker.setActiveHostname(hostname, true);
}
function refreshFocusedTab(settings) {
return activeTab().then(function (tab) {
return trackTab(tab, settings);
}).catch(function () {
return activityTracker.setActiveHostname('', false);
});
}
function configurePassiveActivity(settings) {
return activityTracker.setEnabled(settings.passiveActivityEnabled).then(function () {
if (!settings.passiveActivityEnabled) return undefined;
return refreshFocusedTab(settings);
});
}
function flushPassiveActivity() {
return getSettings().then(function (settings) {
if (!settings.passiveActivityEnabled) return activityTracker.setEnabled(false);
return activityTracker.setEnabled(true).then(function () {
return refreshFocusedTab(settings);
}).then(function () {
return activityTracker.flush();
});
});
}
function setupPassiveActivity() {
if (ext.alarms && ext.alarms.create) {
ext.alarms.create(ACTIVITY_FLUSH_ALARM, { periodInMinutes: 5 });
}
if (ext.idle && ext.idle.setDetectionInterval) {
ext.idle.setDetectionInterval(60);
}
return getSettings().then(configurePassiveActivity);
}
function captureFromInfo(kind, info, tab) {
return protocol.buildCapture({
kind: kind,
@ -204,14 +123,11 @@
return Promise.all([
getSettings(),
queue.list(),
ext.storage.local.get(STATUS_KEY),
activityTracker.getState()
ext.storage.local.get(STATUS_KEY)
]).then(function (results) {
return {
settings: results[0],
pendingCount: results[1].length,
pendingActivityCount: results[3].pendingBatches.length,
activityState: results[3],
status: results[2] && results[2][STATUS_KEY] || {}
};
});
@ -254,24 +170,19 @@
}
function handleMessage(message) {
return ready.then(function () {
if (!message || message.type !== 'verstak.capture') return undefined;
if (message.action === 'getState') return getState();
if (message.action === 'saveSettings') {
return saveSettings(message.settings).then(function () {
return setupContextMenus();
}).then(function () {
return getSettings().then(configurePassiveActivity);
}).then(function () {
return setStatus({ receiverReachable: null, lastResult: 'settings-saved', lastError: '' });
}).then(getState);
}
if (message.action === 'retryPending') return retryPending().then(getState);
if (message.action === 'retryPendingActivity') return activityTracker.retryPending().then(getState);
return activeTab().then(function (tab) {
return sendOrQueue(captureFromInfo(message.kind || 'page', message, tab));
if (!message || message.type !== 'verstak.capture') return Promise.resolve(undefined);
if (message.action === 'getState') return getState();
if (message.action === 'saveSettings') {
return saveSettings(message.settings).then(function () {
return setupContextMenus();
}).then(function () {
return setStatus({ receiverReachable: null, lastResult: 'settings-saved', lastError: '' });
}).then(getState);
});
}
if (message.action === 'retryPending') return retryPending().then(getState);
return activeTab().then(function (tab) {
return sendOrQueue(captureFromInfo(message.kind || 'page', message, tab));
}).then(getState);
}
ext.runtime.onMessage.addListener(function (message, sender, sendResponse) {
@ -280,63 +191,4 @@
});
return true;
});
if (ext.tabs && ext.tabs.onActivated) {
ext.tabs.onActivated.addListener(function (info) {
if (!isFocusedWindow(info.windowId) || !ext.tabs.get) return;
activeTabID = info.tabId;
ready.then(function () { return getSettings(); }).then(function (settings) {
return ext.tabs.get(info.tabId).then(function (tab) { return trackTab(tab, settings); });
}).catch(function () {});
});
}
if (ext.tabs && ext.tabs.onUpdated) {
ext.tabs.onUpdated.addListener(function (tabID, changeInfo, tab) {
if (tabID !== activeTabID || (!changeInfo.url && !(tab && tab.url))) return;
ready.then(function () { return getSettings(); }).then(function (settings) {
return trackTab(tab || { id: tabID, url: changeInfo.url }, settings);
}).catch(function () {});
});
}
if (ext.windows && ext.windows.onFocusChanged) {
ext.windows.onFocusChanged.addListener(function (windowID) {
focusedWindowID = windowID;
if (windowID === (ext.windows.WINDOW_ID_NONE == null ? -1 : ext.windows.WINDOW_ID_NONE)) {
activityTracker.setActiveHostname('', false).catch(function () {});
return;
}
ready.then(function () { return getSettings(); }).then(refreshFocusedTab).catch(function () {});
});
}
if (ext.idle && ext.idle.onStateChanged) {
ext.idle.onStateChanged.addListener(function (state) {
if (state === 'active') {
ready.then(function () { return getSettings(); }).then(refreshFocusedTab).catch(function () {});
return;
}
activityTracker.setActiveHostname('', false).catch(function () {});
});
}
if (ext.alarms && ext.alarms.onAlarm) {
ext.alarms.onAlarm.addListener(function (alarm) {
if (alarm && alarm.name === ACTIVITY_FLUSH_ALARM) flushPassiveActivity().catch(function () {});
});
}
if (ext.tabs && ext.tabs.onRemoved) {
ext.tabs.onRemoved.addListener(function (tabID) {
if (tabID === activeTabID) {
activeTabID = null;
activityTracker.setActiveHostname('', false).catch(function () {});
}
});
}
var ready = activityTracker.initialize().then(setupPassiveActivity).catch(function (error) {
console.warn('[verstak] passive activity initialization failed:', error);
});
})();

View File

@ -1,34 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/hostname-normalization-v1.json",
"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.",
"version": 1,
"bare": [
{ "input": "example.com", "output": "example.com" },
{ "input": " Example.COM. ", "output": "example.com" },
{ "input": "пример.рф", "output": "xn--e1afmkfd.xn--p1ai" },
{ "input": "bücher.example", "output": "xn--bcher-kva.example" },
{ "input": "127.0.0.1", "output": "127.0.0.1" },
{ "input": "[2001:db8::1]", "output": "2001:db8::1" },
{ "input": "localhost", "output": "localhost" },
{ "input": "intranet", "output": "intranet" },
{ "input": "", "output": "" },
{ "input": "https://example.com", "output": "" },
{ "input": "example.com:443", "output": "" },
{ "input": "user@example.com", "output": "" },
{ "input": "bad host", "output": "" },
{ "input": "example..com", "output": "" },
{ "input": "example.com..", "output": "" },
{ "input": "127.000.000.001", "output": "" },
{ "input": "[2001:db8::1]:443", "output": "" },
{ "input": "a...............................................................example", "output": "" }
],
"url": [
{ "input": "https://пример.рф/path", "output": "xn--e1afmkfd.xn--p1ai" },
{ "input": "http://Example.COM.:8080/path", "output": "example.com" },
{ "input": "https://[2001:db8::1]/", "output": "2001:db8::1" },
{ "input": "ftp://example.com/path", "output": "" },
{ "input": "not a URL", "output": "" }
]
}

View File

@ -1,86 +0,0 @@
(function (root) {
'use strict';
var MAX_DNS_HOSTNAME_LENGTH = 253;
var MAX_DNS_LABEL_LENGTH = 63;
function trimmedString(value) {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeIPv4(value) {
var parts = value.split('.');
if (parts.length !== 4) return '';
for (var i = 0; i < parts.length; i += 1) {
if (!/^(0|[1-9][0-9]{0,2})$/.test(parts[i])) return '';
if (Number(parts[i]) > 255) return '';
}
return parts.join('.');
}
function normalizeIPv6(value) {
try {
var parsed = new URL('http://[' + value + ']');
var hostname = parsed.hostname;
if (hostname[0] !== '[' || hostname[hostname.length - 1] !== ']') return '';
return hostname.slice(1, -1).toLowerCase();
} catch (_) {
return '';
}
}
function normalizeDNS(value) {
var ascii;
try {
ascii = new URL('http://' + value).hostname.toLowerCase();
} catch (_) {
return '';
}
if (!ascii || ascii[0] === '[' || ascii.indexOf(':') !== -1) return '';
if (ascii[ascii.length - 1] === '.') ascii = ascii.slice(0, -1);
if (!ascii || ascii[ascii.length - 1] === '.' || ascii.length > MAX_DNS_HOSTNAME_LENGTH) return '';
var labels = ascii.split('.');
for (var i = 0; i < labels.length; i += 1) {
if (labels[i].length === 0 || labels[i].length > MAX_DNS_LABEL_LENGTH) return '';
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(labels[i])) return '';
}
return ascii;
}
function normalizeHostnameV1(input) {
var value = trimmedString(input);
if (!value || /[\s\\/?#@]/.test(value)) return '';
if (value[0] === '[' || value[value.length - 1] === ']') {
if (value[0] !== '[' || value[value.length - 1] !== ']') return '';
return normalizeIPv6(value.slice(1, -1));
}
if (value.indexOf(':') !== -1) return '';
if (value[value.length - 1] === '.') value = value.slice(0, -1);
if (!value || value[value.length - 1] === '.') return '';
if (/^[0-9.]+$/.test(value)) return normalizeIPv4(value);
return normalizeDNS(value);
}
function normalizeURLHostnameV1(input) {
var value = trimmedString(input);
if (!value) return '';
try {
var url = new URL(value);
if (url.protocol !== 'http:' && url.protocol !== 'https:') return '';
return normalizeHostnameV1(url.hostname);
} catch (_) {
return '';
}
}
var api = {
normalizeHostnameV1: normalizeHostnameV1,
normalizeURLHostnameV1: normalizeURLHostnameV1
};
root.VerstakBrowser = Object.assign(root.VerstakBrowser || {}, api);
if (typeof module !== 'undefined') module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : this);

View File

@ -2,20 +2,16 @@
"popup.subtitle": "Browser Inbox",
"label.receiver": "Receiver",
"label.pending": "Pending",
"label.activityPending": "Activity batches",
"label.url": "URL",
"label.file": "File",
"label.receiverUrl": "Receiver URL",
"label.pairingToken": "Pairing token",
"label.language": "Language",
"label.passiveActivity": "Track active domain time",
"label.passiveActivityExclusions": "Excluded domains",
"action.sendPage": "Send Page",
"action.sendFile": "Send File",
"action.retryPending": "Retry Pending",
"action.save": "Save",
"hint.contextMenu": "Selection and link captures are available from the page context menu.",
"hint.passiveActivityDisclosure": "Optional. Collects only the active domain and time spent there. It never sends page URLs, titles, text, keystrokes, or page contents.",
"receiver.online": "Online",
"receiver.offline": "Offline",
"receiver.unknown": "Unknown",

View File

@ -2,20 +2,16 @@
"popup.subtitle": "Входящие из браузера",
"label.receiver": "Приёмник",
"label.pending": "В очереди",
"label.activityPending": "Пакеты активности",
"label.url": "URL",
"label.file": "Файл",
"label.receiverUrl": "URL приёмника",
"label.pairingToken": "Токен сопряжения",
"label.language": "Язык",
"label.passiveActivity": "Учитывать время на активном домене",
"label.passiveActivityExclusions": "Исключённые домены",
"action.sendPage": "Отправить страницу",
"action.sendFile": "Отправить файл",
"action.retryPending": "Повторить отправку",
"action.save": "Сохранить",
"hint.contextMenu": "Выделение и ссылки можно отправить через контекстное меню страницы.",
"hint.passiveActivityDisclosure": "Необязательно. Собираются только активный домен и время на нём. URL страниц, заголовки, текст, нажатия клавиш и содержимое страниц не отправляются.",
"receiver.online": "Доступен",
"receiver.offline": "Недоступен",
"receiver.unknown": "Неизвестно",

View File

@ -126,19 +126,6 @@ input {
color: #e5e7eb;
}
textarea {
box-sizing: border-box;
width: 100%;
margin-top: 5px;
border: 1px solid #374151;
border-radius: 4px;
padding: 7px 8px;
resize: vertical;
background: #0f172a;
color: #e5e7eb;
font: inherit;
}
select {
box-sizing: border-box;
width: 100%;
@ -155,23 +142,6 @@ input[type="file"] {
font-size: 12px;
}
.tracking-toggle {
display: flex;
align-items: center;
gap: 7px;
color: #e8ecf3 !important;
cursor: pointer;
}
.tracking-toggle input {
width: auto;
margin: 0;
}
.tracking-settings .hint {
margin: 8px 0 4px;
}
.hint {
margin: 0;
font-size: 12px;

View File

@ -24,10 +24,6 @@
<span id="pending-label">Pending</span>
<strong id="pending-count">0</strong>
</div>
<div class="row">
<span id="activity-pending-label">Activity batches</span>
<strong id="activity-pending-count">0</strong>
</div>
<div class="row url-row">
<span id="url-label">URL</span>
<code id="receiver-url"></code>
@ -59,16 +55,6 @@
<button id="save-settings" class="secondary">Save</button>
</section>
<section class="settings tracking-settings">
<label class="tracking-toggle" for="passive-activity-enabled">
<input id="passive-activity-enabled" type="checkbox">
<span id="passive-activity-label">Track active domain time</span>
</label>
<p id="passive-activity-disclosure" class="hint">Optional. Collects only the active domain and time spent there. It never sends page URLs, titles, text, keystrokes, or page contents.</p>
<label id="passive-activity-exclusions-label" for="passive-activity-exclusions">Excluded domains</label>
<textarea id="passive-activity-exclusions" rows="3" spellcheck="false" placeholder="youtube.com"></textarea>
</section>
<p id="context-menu-hint" class="hint">Selection and link captures are available from the page context menu.</p>
<p id="status"></p>
</main>

View File

@ -11,10 +11,7 @@
var languageSelectEl = document.getElementById('language-select');
var fileInputEl = document.getElementById('file-input');
var pendingCountEl = document.getElementById('pending-count');
var pendingActivityCountEl = document.getElementById('activity-pending-count');
var statusDotEl = document.getElementById('status-dot');
var passiveActivityEnabledEl = document.getElementById('passive-activity-enabled');
var passiveActivityExclusionsEl = document.getElementById('passive-activity-exclusions');
var MAX_FILE_TEXT_LENGTH = 2 * 1024 * 1024;
var MAX_FILE_BYTES = 8 * 1024 * 1024;
var catalogs = { en: {}, ru: {} };
@ -26,7 +23,6 @@
subtitle: 'popup.subtitle',
'receiver-label': 'label.receiver',
'pending-label': 'label.pending',
'activity-pending-label': 'label.activityPending',
'url-label': 'label.url',
'file-label': 'label.file',
'receiver-url-label': 'label.receiverUrl',
@ -39,10 +35,7 @@
'context-menu-hint': 'hint.contextMenu',
'language-system-option': 'language.system',
'language-en-option': 'language.en',
'language-ru-option': 'language.ru',
'passive-activity-label': 'label.passiveActivity',
'passive-activity-disclosure': 'hint.passiveActivityDisclosure',
'passive-activity-exclusions-label': 'label.passiveActivityExclusions'
'language-ru-option': 'language.ru'
};
function browserLocale() {
@ -129,16 +122,9 @@
currentState = state || {};
var settings = currentState.settings || {};
pendingCountEl.textContent = String(currentState.pendingCount || 0);
pendingActivityCountEl.textContent = String(currentState.pendingActivityCount || 0);
receiverUrlEl.textContent = settings.receiverUrl || '';
if (document.activeElement !== receiverInputEl) receiverInputEl.value = settings.receiverUrl || '';
if (document.activeElement !== receiverTokenInputEl) receiverTokenInputEl.value = settings.receiverToken || '';
passiveActivityEnabledEl.checked = settings.passiveActivityEnabled === true;
if (document.activeElement !== passiveActivityExclusionsEl) {
passiveActivityExclusionsEl.value = Array.isArray(settings.passiveActivityExcludedDomains)
? settings.passiveActivityExcludedDomains.join('\n')
: '';
}
applyReceiverState(currentState);
}
@ -156,11 +142,7 @@
return {
receiverUrl: receiverInputEl.value.trim(),
receiverToken: receiverTokenInputEl.value.trim(),
language: currentPreference,
passiveActivityEnabled: passiveActivityEnabledEl.checked === true,
passiveActivityExcludedDomains: passiveActivityExclusionsEl.value.split(/[\n,]/).map(function (value) {
return value.trim();
}).filter(Boolean)
language: currentPreference
};
}

View File

@ -3,7 +3,6 @@
var CAPTURE_SCHEMA_VERSION = 1;
var DEFAULT_RECEIVER_URL = 'http://127.0.0.1:47731/api/browser-inbox/v1/captures';
var DEFAULT_ACTIVITY_URL = 'http://127.0.0.1:47731/api/browser-activity/v1/batches';
var MAX_FILE_TEXT_LENGTH = 2 * 1024 * 1024;
var MAX_FILE_BYTES = 8 * 1024 * 1024;
@ -30,9 +29,11 @@
}
function hostname(url) {
return root.VerstakBrowser.normalizeURLHostnameV1
? root.VerstakBrowser.normalizeURLHostnameV1(url)
: '';
try {
return new URL(url).hostname;
} catch (_) {
return '';
}
}
function buildCapture(input) {
@ -99,7 +100,6 @@
var api = {
CAPTURE_SCHEMA_VERSION: CAPTURE_SCHEMA_VERSION,
DEFAULT_RECEIVER_URL: DEFAULT_RECEIVER_URL,
DEFAULT_ACTIVITY_URL: DEFAULT_ACTIVITY_URL,
MAX_FILE_TEXT_LENGTH: MAX_FILE_TEXT_LENGTH,
MAX_FILE_BYTES: MAX_FILE_BYTES,
buildCapture: buildCapture,