Skip to content

Commit 96368a9

Browse files
committed
release: v1.2.0
This update include a breaking change with the monloader pairing mechanism. Update monloader to >v1.2.0 and use the pairing mechanism. ### Added - One-click "connect to monloader": pair and approve in monloader's settings. ### Changed - Changing the monloader URL now clears the stored token, so you re-pair for the new instance. ### Removed - Manual API token field in the options page.
1 parent 3d17760 commit 96368a9

10 files changed

Lines changed: 85 additions & 25 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
node-version: "20"
2525

2626
- name: Build bundles
27-
run: npm run build
27+
run: node build.js
2828

2929
- name: Extract changelog entry
3030
run: |

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## [v1.2.0] - 2026-07-01
4+
5+
This update include a breaking change with the monloader pairing mechanism. Update monloader to >v1.2.0 and use the pairing mechanism.
6+
7+
### Added
8+
- One-click "connect to monloader": pair and approve in monloader's settings.
9+
### Changed
10+
- Changing the monloader URL now clears the stored token, so you re-pair for the new instance.
11+
### Removed
12+
- Manual API token field in the options page.
13+
314
## [v1.1.0] - 2026-06-25
415
### Added
516
- Scan chooser shows each image's resolutions as inline tokens.

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ Open the extension's options and set your monloader URL and token.
5757
Build the per-browser bundles:
5858

5959
```
60-
npm install
61-
npm run build # writes dist/chrome and dist/firefox
60+
node build.js # writes dist/chrome and dist/firefox
6261
```
6362

6463
## Attribution and license

VERSION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v1.1.0
1+
v1.2.0

package.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "monsender",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "Send the current page or its images to your monloader download queue.",
55
"private": true,
66
"type": "commonjs",
@@ -25,9 +25,5 @@
2525
"repository": {
2626
"type": "git",
2727
"url": "https://github.com/leqwin/monsender"
28-
},
29-
"devDependencies": {
30-
"jsdom": "^24.0.0",
31-
"playwright": "^1.60.0"
3228
}
3329
}

src/lib/api.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,20 @@
115115
return req(base(cfg) + '/api/v1/sites' + p, { headers: headers(cfg, false) });
116116
}
117117

118-
return { enqueue, enqueueBatch, listQueue, retry, continueJob, continueAll, remove, health, listSites };
118+
// pair offers a pairing to monloader; the operator approves there. No token is
119+
// sent (obtaining one is the point). Returns { request_id } on success.
120+
async function pair(cfg) {
121+
return req(base(cfg) + '/api/v1/pair/request', {
122+
method: 'POST', headers: { 'Content-Type': 'application/json' },
123+
body: JSON.stringify({ app: 'monsender', requested_scopes: ['read', 'write'] })
124+
});
125+
}
126+
127+
// pairStatus polls a pending request; once approved, data.token carries the
128+
// issued bearer token (delivered once).
129+
async function pairStatus(cfg, id) {
130+
return req(base(cfg) + '/api/v1/pair/status?id=' + encodeURIComponent(id), { headers: {} });
131+
}
132+
133+
return { enqueue, enqueueBatch, listQueue, retry, continueJob, continueAll, remove, health, listSites, pair, pairStatus };
119134
});

src/lib/ui.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ input:focus, select:focus, textarea:focus { outline: 1px solid var(--accent); bo
8484
}
8585
.field .hint { flex-basis: 100%; color: var(--fg-dim); font-size: 11px; padding-left: 158px; }
8686
.actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; margin-top: 10px; }
87-
.actions .conn { margin-right: auto; }
87+
.actions + .conn { margin-top: 8px; }
8888
/* Brand logo mirrors monloader's topbar logo; the wordmark treatment (caps,
8989
bold, letter-spacing) lives with each surface's brand header. */
9090
.logo { height: 40px; width: auto; flex-shrink: 0; }

src/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "monsender",
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"description": "Send the current page or its images to your monloader download queue.",
66
"permissions": ["activeTab", "scripting", "storage", "contextMenus"],
77
"optional_host_permissions": ["http://*/*", "https://*/*"],

src/options/options.html

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,12 @@ <h2>monloader</h2>
2222
<label for="baseUrl">URL</label>
2323
<input id="baseUrl" type="text" placeholder="http://localhost:8081" autocomplete="off" spellcheck="false" />
2424
</div>
25-
<div class="field">
26-
<label for="token">API token</label>
27-
<input id="token" type="password" placeholder="(not set)" autocomplete="off" />
28-
</div>
2925
<div class="actions">
30-
<span id="conn" class="conn"></span>
26+
<button id="connect" type="button">connect to monloader</button>
3127
<button id="test" type="button">test connection</button>
3228
<button id="save" type="button">save</button>
3329
</div>
30+
<span id="conn" class="conn"></span>
3431
<div id="flash"></div>
3532
</div>
3633

src/options/options.js

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@
2828
}
2929

3030
const normalizeUrl = urls.normalizeUrl;
31+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3132

3233
async function load() {
3334
const s = await storage.getSettings();
3435
$('baseUrl').value = s.baseUrl;
35-
$('token').value = '';
36-
$('token').placeholder = s.token ? '(set - leave blank to keep)' : '(not set)';
3736
$('wait').value = s.waitSeconds;
3837
$('scanCap').value = s.scanCap;
3938
$('minSize').value = s.minImageSize;
@@ -48,16 +47,14 @@
4847
minImageSize: clampInt($('minSize').value, 0, 4000, 64),
4948
previews: $('previews').checked
5049
};
51-
const token = $('token').value;
52-
if (token) patch.token = token; // keep existing unless a new one is typed
5350
return patch;
5451
}
5552

5653
async function cfgFromForm() {
5754
const s = await storage.getSettings();
5855
return {
5956
baseUrl: normalizeUrl($('baseUrl').value) || s.baseUrl,
60-
token: $('token').value || s.token
57+
token: s.token
6158
};
6259
}
6360

@@ -77,16 +74,60 @@
7774

7875
const patch = readForm();
7976
patch.baseUrl = url;
77+
// A token is bound to the instance that issued it; a changed URL drops the
78+
// stored token so the old key is never sent to a different monloader.
79+
const urlChanged = !!(lastSavedUrl && lastSavedUrl !== url);
80+
if (urlChanged) {
81+
const cur = await storage.getSettings();
82+
if (cur.token) patch.token = '';
83+
}
8084
const saved = await storage.setSettings(patch);
8185
lastSavedUrl = saved.baseUrl;
82-
$('token').value = '';
83-
$('token').placeholder = saved.token ? '(set - leave blank to keep)' : '(not set)';
84-
if (granted) setFlash('saved', 'flash-ok');
86+
if (granted) setFlash(urlChanged && !saved.token ? 'saved; URL changed, cleared the stored token' : 'saved', 'flash-ok');
8587
// Always refresh the dot: when the grant was denied, test() short-circuits
8688
// to the "allow host access" state instead of leaving a stale indicator.
8789
test();
8890
}
8991

92+
// connect pairs with monloader without copying keys: it requests a pairing,
93+
// the operator approves in monloader, and the issued token is stored here.
94+
async function connect() {
95+
const url = normalizeUrl($('baseUrl').value);
96+
$('baseUrl').value = url;
97+
if (!url) { setFlash('set a monloader URL first', 'flash-err'); return; }
98+
let granted = false;
99+
try {
100+
const oldUrl = lastSavedUrl && lastSavedUrl !== url ? lastSavedUrl : null;
101+
granted = await perms.syncOrigin(url, oldUrl);
102+
} catch (e) { granted = false; }
103+
if (!granted) { setFlash('host access for ' + url + ' was not granted', 'flash-warn'); return; }
104+
105+
setFlash('requesting pairing...', 'flash-warn');
106+
const cfg = { baseUrl: url };
107+
const r = await api.pair(cfg);
108+
if (!r.ok || !(r.data && r.data.request_id)) {
109+
setFlash((r.data && r.data.error) || (r.networkError ? 'monloader unreachable' : 'pairing request failed'), 'flash-err');
110+
return;
111+
}
112+
setFlash('approve the request in monloader (settings → authentication), waiting...', 'flash-warn');
113+
const id = r.data.request_id;
114+
for (let i = 0; i < 60; i++) {
115+
await sleep(1500);
116+
const st = await api.pairStatus(cfg, id);
117+
if (!st.ok || !st.data) continue;
118+
if (st.data.status === 'approved' && st.data.token) {
119+
const saved = await storage.setSettings({ baseUrl: url, token: st.data.token });
120+
lastSavedUrl = saved.baseUrl;
121+
setFlash('paired with monloader', 'flash-ok');
122+
test();
123+
return;
124+
}
125+
if (st.data.status === 'denied') { setFlash('pairing was denied in monloader', 'flash-err'); return; }
126+
if (st.data.status === 'expired') { setFlash('pairing request expired; try again', 'flash-err'); return; }
127+
}
128+
setFlash('timed out waiting for approval; try again', 'flash-err');
129+
}
130+
90131
async function test() {
91132
const cfg = await cfgFromForm();
92133
if (!cfg.baseUrl) { setConn('down', 'no URL'); return; }
@@ -111,6 +152,7 @@
111152

112153
$('save').addEventListener('click', save);
113154
$('test').addEventListener('click', test);
155+
$('connect').addEventListener('click', connect);
114156

115157
// Scripts are at the end of <body>, so the DOM above exists. Init now.
116158
load().then(test);

0 commit comments

Comments
 (0)