ultimate-jekyll-manager 1.9.26 → 1.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,14 @@ export default function () {
8
8
  const $status = document.getElementById('token-status');
9
9
  const $error = document.getElementById('token-error');
10
10
  const $errorMessage = document.getElementById('token-error-message');
11
+ const $spinner = document.getElementById('token-spinner');
12
+ const $actions = document.getElementById('token-actions');
13
+ const $retry = document.getElementById('token-retry');
14
+
15
+ // Retry re-runs the whole flow — reloading re-attempts auth + token generation
16
+ if ($retry) {
17
+ $retry.addEventListener('click', () => window.location.reload());
18
+ }
11
19
 
12
20
  // Get URL params
13
21
  const url = new URL(window.location.href);
@@ -83,7 +91,7 @@ export default function () {
83
91
  const returnUrl = new URL(authReturnUrl);
84
92
  returnUrl.searchParams.set('authToken', token);
85
93
 
86
- // LEGACY: Reformat token for desktop app deep links
94
+ // LEGACY: Add the legacy payload shape for old desktop app deep links
87
95
  // TODO: Remove this block when legacy desktop app support is no longer needed
88
96
  _legacyTranslateTokenRedirect(returnUrl, token);
89
97
 
@@ -101,6 +109,7 @@ export default function () {
101
109
  // Extension background will detect this and close the tab
102
110
  url.searchParams.set('authToken', token);
103
111
  window.history.replaceState({}, '', url.toString());
112
+ stopSpinner();
104
113
  updateStatus('You can close this tab now.');
105
114
  }
106
115
  } catch (error) {
@@ -150,8 +159,10 @@ export default function () {
150
159
  $status.appendChild($p);
151
160
  }
152
161
 
153
- // Show error message
162
+ // Show error message + swap the spinner for the retry / go-home actions
154
163
  function showError(message) {
164
+ stopSpinner();
165
+
155
166
  if ($error && $errorMessage) {
156
167
  $errorMessage.textContent = message;
157
168
  $error.classList.remove('d-none');
@@ -159,14 +170,25 @@ export default function () {
159
170
  if ($status) {
160
171
  $status.classList.add('d-none');
161
172
  }
173
+ if ($actions) {
174
+ $actions.classList.remove('d-none');
175
+ }
176
+ }
177
+
178
+ // Stop the loading spinner once the flow reaches a terminal state
179
+ function stopSpinner() {
180
+ if ($spinner) {
181
+ $spinner.classList.add('d-none');
182
+ }
162
183
  }
163
184
 
164
- // LEGACY: Reformat token for desktop app deep links
165
- // Legacy desktop apps expect ?payload={"token":"X"} instead of ?authToken=X for custom protocol URLs
185
+ // LEGACY: Add the legacy token shape for desktop app deep links
186
+ // Legacy desktop apps read ?payload={"token":"X"} on custom protocol URLs — ADDED
187
+ // alongside ?authToken=X (never replacing it): old apps read payload and ignore
188
+ // authToken, modern apps (Electron Manager) read authToken and ignore payload.
166
189
  // TODO: Remove this function AND its call above when legacy desktop app support is no longer needed
167
190
  function _legacyTranslateTokenRedirect(returnUrl, token) {
168
191
  if (returnUrl.protocol !== 'http:' && returnUrl.protocol !== 'https:') {
169
- returnUrl.searchParams.delete('authToken');
170
192
  returnUrl.searchParams.set('payload', JSON.stringify({ token: token }));
171
193
  }
172
194
  }
@@ -20,7 +20,7 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
20
20
  </div>
21
21
 
22
22
  <!-- Spinner -->
23
- <div class="text-center mb-4">
23
+ <div id="token-spinner" class="text-center mb-4">
24
24
  <div class="spinner-border text-primary" role="status">
25
25
  <span class="visually-hidden">Loading...</span>
26
26
  </div>
@@ -35,6 +35,12 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
35
35
  <div id="token-error" class="alert alert-danger d-none" role="alert">
36
36
  <strong>Error:</strong> <span id="token-error-message"></span>
37
37
  </div>
38
+
39
+ <!-- Actions (hidden until the flow resolves with an error) -->
40
+ <div id="token-actions" class="text-center d-none">
41
+ <button type="button" id="token-retry" class="btn btn-primary w-100">Try again</button>
42
+ <a href="/" class="btn btn-link btn-sm text-muted d-block mt-2">Go home</a>
43
+ </div>
38
44
  </div>
39
45
  </div>
40
46
  </section>
@@ -129,30 +129,32 @@ async function getHttpsConfig() {
129
129
  // Check if mkcert certificates exist
130
130
  const certPath = path.join(rootPathProject, '.temp');
131
131
 
132
- // Look for any mkcert generated files (handles localhost+2.pem format)
133
- const certFiles = jetpack.find(certPath, { matching: 'localhost*.pem' }) || [];
132
+ // Look for any mkcert generated files. NOTE: mkcert names files after the FIRST
133
+ // SAN host (development.<brand>+N.pem, not localhost*.pem) match *.pem, the
134
+ // same pattern generateMkcertCertificates() finds after generating.
135
+ const certFiles = jetpack.find(certPath, { matching: '*.pem' }) || [];
134
136
  const keyFile = certFiles.find(f => f.includes('-key.pem'));
135
137
  const certFile = certFiles.find(f => !f.includes('-key.pem'));
136
138
 
137
139
  if (keyFile && certFile) {
138
- logger.log('Using mkcert certificates from .temp/');
139
- logger.log(`Certificate: ${certFile}`);
140
- logger.log(`Key: ${keyFile}`);
140
+ const problem = checkCertProblem(certFile);
141
141
 
142
- // Read certificate to check validity
143
- try {
144
- const certContent = jetpack.read(certFile);
145
- if (certContent && certContent.includes('BEGIN CERTIFICATE')) {
146
- logger.log('✅ Certificate appears valid');
147
- }
148
- } catch (e) {
149
- logger.log('⚠️ Could not read certificate');
142
+ if (!problem) {
143
+ logger.log('Using mkcert certificates from .temp/');
144
+ logger.log(`Certificate: ${certFile}`);
145
+ logger.log(`Key: ${keyFile}`);
146
+
147
+ return {
148
+ key: keyFile,
149
+ cert: certFile
150
+ };
150
151
  }
151
152
 
152
- return {
153
- key: keyFile,
154
- cert: certFile
155
- };
153
+ // Stale certs (expired, or issued by a DIFFERENT machine's mkcert CA — e.g.
154
+ // .temp copied over from another Mac) make browsers reject https://localhost:4000
155
+ // outright. Wipe and regenerate against THIS machine's trusted CA.
156
+ logger.log(`Existing certificates are not usable (${problem}) — regenerating...`);
157
+ certFiles.forEach((file) => jetpack.remove(file));
156
158
  }
157
159
 
158
160
  // Try to generate mkcert certificates
@@ -170,6 +172,41 @@ async function getHttpsConfig() {
170
172
  return true;
171
173
  }
172
174
 
175
+ // Returns a reason string when the existing cert must be regenerated, or null
176
+ // when it's usable: unexpired AND signed by this machine's trusted mkcert root CA.
177
+ function checkCertProblem(certFile) {
178
+ const { X509Certificate } = require('crypto');
179
+ const { execSync } = require('child_process');
180
+
181
+ let cert;
182
+ try {
183
+ cert = new X509Certificate(jetpack.read(certFile));
184
+ } catch (e) {
185
+ return 'unreadable certificate';
186
+ }
187
+
188
+ if (new Date(cert.validTo) <= new Date()) {
189
+ return `expired ${cert.validTo}`;
190
+ }
191
+
192
+ // Verify the signature chains to the CURRENT mkcert root CA. If mkcert (or its
193
+ // root) isn't available we can't verify — keep the existing certs rather than
194
+ // breaking the self-signed fallback path.
195
+ try {
196
+ const caRoot = execSync('mkcert -CAROOT', { encoding: 'utf8' }).trim();
197
+ const ca = new X509Certificate(jetpack.read(path.join(caRoot, 'rootCA.pem')));
198
+
199
+ if (!cert.verify(ca.publicKey)) {
200
+ const issuerCN = cert.issuer.split('\n').find((line) => line.startsWith('CN=')) || cert.issuer;
201
+ return `issued by a different CA (${issuerCN})`;
202
+ }
203
+ } catch (e) {
204
+ return null;
205
+ }
206
+
207
+ return null;
208
+ }
209
+
173
210
  // Generate mkcert certificates
174
211
  async function generateMkcertCertificates(certPath) {
175
212
  try {
@@ -0,0 +1,124 @@
1
+ // /token page: when the flow resolves with an error (e.g. token fetch fails) the
2
+ // loading spinner must STOP and be replaced by a "Try again" (reload) button plus
3
+ // a smaller "Go home" link — previously the spinner spun forever behind the error.
4
+ //
5
+ // Each run() is serialized to the browser (Puppeteer), so every test builds the
6
+ // token card DOM (mirrors classy's auth/token.html) and replicates the module's
7
+ // stopSpinner/showError logic inline — the page module can't be imported (ESM +
8
+ // web-manager deps) and module-scope helpers aren't in the browser's scope.
9
+
10
+ module.exports = {
11
+ layer: 'page',
12
+ description: '/token error state swaps the spinner for retry/home actions',
13
+ type: 'group',
14
+ tests: [
15
+ {
16
+ name: 'showError stops the spinner and reveals the retry/home actions',
17
+ run: async (ctx) => {
18
+ const $root = document.createElement('div');
19
+ $root.innerHTML = ''
20
+ + '<div id="token-spinner" class="text-center mb-4"><div class="spinner-border" role="status"></div></div>'
21
+ + '<div id="token-status" class="text-center"><p class="text-muted small">Generating secure token...</p></div>'
22
+ + '<div id="token-error" class="alert alert-danger d-none" role="alert"><strong>Error:</strong> <span id="token-error-message"></span></div>'
23
+ + '<div id="token-actions" class="text-center d-none">'
24
+ + '<button type="button" id="token-retry" class="btn btn-primary w-100">Try again</button>'
25
+ + '<a href="/" class="btn btn-link btn-sm text-muted d-block mt-2">Go home</a>'
26
+ + '</div>';
27
+ document.body.appendChild($root);
28
+
29
+ const $status = $root.querySelector('#token-status');
30
+ const $error = $root.querySelector('#token-error');
31
+ const $errorMessage = $root.querySelector('#token-error-message');
32
+ const $spinner = $root.querySelector('#token-spinner');
33
+ const $actions = $root.querySelector('#token-actions');
34
+
35
+ function stopSpinner() {
36
+ if ($spinner) { $spinner.classList.add('d-none'); }
37
+ }
38
+ function showError(message) {
39
+ stopSpinner();
40
+ if ($error && $errorMessage) {
41
+ $errorMessage.textContent = message;
42
+ $error.classList.remove('d-none');
43
+ }
44
+ if ($status) { $status.classList.add('d-none'); }
45
+ if ($actions) { $actions.classList.remove('d-none'); }
46
+ }
47
+
48
+ // Baseline: spinner visible, actions + error hidden
49
+ ctx.expect($spinner.classList.contains('d-none')).toBe(false);
50
+ ctx.expect($actions.classList.contains('d-none')).toBe(true);
51
+ ctx.expect($error.classList.contains('d-none')).toBe(true);
52
+
53
+ showError('Failed to generate token. Please try again.');
54
+
55
+ // Spinner + status gone, error + actions shown, message set
56
+ ctx.expect($spinner.classList.contains('d-none')).toBe(true);
57
+ ctx.expect($status.classList.contains('d-none')).toBe(true);
58
+ ctx.expect($error.classList.contains('d-none')).toBe(false);
59
+ ctx.expect($actions.classList.contains('d-none')).toBe(false);
60
+ ctx.expect($errorMessage.textContent).toBe('Failed to generate token. Please try again.');
61
+
62
+ $root.remove();
63
+ },
64
+ },
65
+ {
66
+ name: 'retry button click re-runs the flow (reload)',
67
+ run: async (ctx) => {
68
+ const $root = document.createElement('div');
69
+ $root.innerHTML = '<button type="button" id="token-retry" class="btn btn-primary w-100">Try again</button>';
70
+ document.body.appendChild($root);
71
+
72
+ const $retry = $root.querySelector('#token-retry');
73
+
74
+ // Mirror the module's wiring with reload stubbed (can't reload the harness)
75
+ let reloadCount = 0;
76
+ $retry.addEventListener('click', () => { reloadCount++; });
77
+
78
+ $retry.click();
79
+ $retry.click();
80
+
81
+ ctx.expect(reloadCount).toBe(2);
82
+
83
+ $root.remove();
84
+ },
85
+ },
86
+ {
87
+ name: 'go-home link points at the site root',
88
+ run: async (ctx) => {
89
+ const $root = document.createElement('div');
90
+ $root.innerHTML = '<div id="token-actions"><a href="/" class="btn btn-link btn-sm text-muted d-block mt-2">Go home</a></div>';
91
+ document.body.appendChild($root);
92
+
93
+ const $home = $root.querySelector('#token-actions a');
94
+
95
+ ctx.expect($home).toBeTruthy();
96
+ ctx.expect($home.getAttribute('href')).toBe('/');
97
+
98
+ $root.remove();
99
+ },
100
+ },
101
+ {
102
+ name: 'terminal success (close-tab flow) also stops the spinner',
103
+ run: async (ctx) => {
104
+ const $root = document.createElement('div');
105
+ $root.innerHTML = ''
106
+ + '<div id="token-spinner" class="text-center mb-4"><div class="spinner-border" role="status"></div></div>'
107
+ + '<div id="token-actions" class="text-center d-none"></div>';
108
+ document.body.appendChild($root);
109
+
110
+ const $spinner = $root.querySelector('#token-spinner');
111
+ const $actions = $root.querySelector('#token-actions');
112
+
113
+ // The extension "You can close this tab now." branch calls stopSpinner()
114
+ if ($spinner) { $spinner.classList.add('d-none'); }
115
+
116
+ ctx.expect($spinner.classList.contains('d-none')).toBe(true);
117
+ // No error, so the retry/home actions stay hidden
118
+ ctx.expect($actions.classList.contains('d-none')).toBe(true);
119
+
120
+ $root.remove();
121
+ },
122
+ },
123
+ ],
124
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.9.26",
3
+ "version": "1.9.28",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -116,7 +116,7 @@
116
116
  "prettier": "^3.9.4",
117
117
  "sass": "^1.101.0",
118
118
  "spellchecker": "^3.7.1",
119
- "web-manager": "^4.3.2",
119
+ "web-manager": "^4.3.4",
120
120
  "webpack": "^5.108.3",
121
121
  "wonderful-fetch": "^2.0.5",
122
122
  "wonderful-version": "^1.3.2",