sitevision-cli 1.0.0-beta.5 → 1.0.0-beta.7

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.
@@ -33,6 +33,30 @@ const RETRY_BASE_DELAY_MS = 1000;
33
33
  function createBasicAuth(username, password) {
34
34
  return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
35
35
  }
36
+ /** Single source of the 401 message, worded for the auth kind actually used. */
37
+ function unauthorizedMessage(kind) {
38
+ switch (kind) {
39
+ case 'bearer':
40
+ return 'Unauthorized. The access token was rejected or has expired.';
41
+ case 'cookie':
42
+ return 'Unauthorized. The session cookie was rejected or has expired — log in again.';
43
+ default:
44
+ return 'Unauthorized. Check username and password.';
45
+ }
46
+ }
47
+ /** Pick cookie > bearer > basic based on what the deploy config carries. */
48
+ function configAuth(config) {
49
+ if (config.sessionCookie) {
50
+ return { auth: { cookie: config.sessionCookie }, kind: 'cookie' };
51
+ }
52
+ if (config.accessToken) {
53
+ return { auth: { token: config.accessToken }, kind: 'bearer' };
54
+ }
55
+ return {
56
+ auth: { username: config.username, password: config.password ?? '' },
57
+ kind: 'basic',
58
+ };
59
+ }
36
60
  /**
37
61
  * Generate a random boundary for multipart form data
38
62
  */
@@ -71,7 +95,18 @@ export function makeRequest(url, options) {
71
95
  ...options.headers,
72
96
  };
73
97
  if (options.auth) {
74
- headers['Authorization'] = createBasicAuth(options.auth.username, options.auth.password);
98
+ if ('cookie' in options.auth) {
99
+ headers['Cookie'] = options.auth.cookie;
100
+ // Session-authenticated state-changing calls typically need this to
101
+ // pass Sitevision's CSRF guard, unlike Basic-auth requests.
102
+ headers['X-Requested-With'] ??= 'XMLHttpRequest';
103
+ }
104
+ else if ('token' in options.auth) {
105
+ headers['Authorization'] = `Bearer ${options.auth.token}`;
106
+ }
107
+ else {
108
+ headers['Authorization'] = createBasicAuth(options.auth.username, options.auth.password);
109
+ }
75
110
  }
76
111
  const requestOptions = {
77
112
  hostname: parsedUrl.hostname,
@@ -145,6 +180,31 @@ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
145
180
  export function looksLikeZip(body) {
146
181
  return body.length >= 4 && body.subarray(0, 4).equals(ZIP_MAGIC);
147
182
  }
183
+ /**
184
+ * A stale Sitevision session usually answers with a redirect to the login page
185
+ * or a 200 carrying an HTML login form — not a clean 401. Detect both so cookie
186
+ * auth can drop the dead session and re-login instead of showing a generic error.
187
+ */
188
+ export function looksLikeAuthExpired(statusCode, body, headers) {
189
+ if (statusCode === 401)
190
+ return true;
191
+ if (statusCode >= 300 && statusCode < 400)
192
+ return true;
193
+ if (statusCode === 200) {
194
+ const contentType = headers['content-type'] ?? '';
195
+ if (contentType.includes('html'))
196
+ return true;
197
+ const head = body
198
+ .subarray(0, 64)
199
+ .toString('utf8')
200
+ .trimStart()
201
+ .toLowerCase();
202
+ if (head.startsWith('<!doctype html') || head.startsWith('<html')) {
203
+ return true;
204
+ }
205
+ }
206
+ return false;
207
+ }
148
208
  // =============================================================================
149
209
  // SIGNING API
150
210
  // =============================================================================
@@ -211,7 +271,7 @@ export async function signApp(zipPath, credentials, outputPath) {
211
271
  // Auth failures will not resolve on retry.
212
272
  return {
213
273
  success: false,
214
- error: 'Unauthorized. Check username and password.',
274
+ error: unauthorizedMessage('basic'),
215
275
  };
216
276
  }
217
277
  lastError = `Signing failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`;
@@ -259,6 +319,7 @@ export async function deployApp(zipPath, config, appType, force = false) {
259
319
  // Create multipart form data
260
320
  const boundary = generateBoundary();
261
321
  const { body, contentType } = createMultipartFormData(zipPath, 'file', boundary);
322
+ const { auth, kind } = configAuth(config);
262
323
  try {
263
324
  const response = await makeRequest(url, {
264
325
  method: 'POST',
@@ -267,11 +328,25 @@ export async function deployApp(zipPath, config, appType, force = false) {
267
328
  'Content-Length': String(body.length),
268
329
  },
269
330
  body,
270
- auth: {
271
- username: config.username,
272
- password: config.password,
273
- },
331
+ auth,
274
332
  });
333
+ if (response.statusCode === 401) {
334
+ return {
335
+ success: false,
336
+ error: unauthorizedMessage(kind),
337
+ authExpired: true,
338
+ };
339
+ }
340
+ // A cookie session fails without a clean 401: a redirect to login or a
341
+ // 200 carrying an HTML login page. Flag it so the caller re-authenticates.
342
+ if (kind === 'cookie' &&
343
+ looksLikeAuthExpired(response.statusCode, response.body, response.headers)) {
344
+ return {
345
+ success: false,
346
+ error: unauthorizedMessage('cookie'),
347
+ authExpired: true,
348
+ };
349
+ }
275
350
  if (response.statusCode === 200) {
276
351
  // Try to parse response for executable ID
277
352
  let executableId;
@@ -288,12 +363,6 @@ export async function deployApp(zipPath, config, appType, force = false) {
288
363
  message: 'Deployment successful',
289
364
  };
290
365
  }
291
- if (response.statusCode === 401) {
292
- return {
293
- success: false,
294
- error: 'Unauthorized. Check username and password.',
295
- };
296
- }
297
366
  if (response.statusCode === 409) {
298
367
  return {
299
368
  success: false,
@@ -358,6 +427,7 @@ export async function createAddon(config, appType) {
358
427
  name: config.addonName,
359
428
  category: 'Other',
360
429
  });
430
+ const { auth, kind } = configAuth(config);
361
431
  try {
362
432
  const response = await makeRequest(url, {
363
433
  method: 'POST',
@@ -366,10 +436,7 @@ export async function createAddon(config, appType) {
366
436
  'Content-Length': String(Buffer.byteLength(body)),
367
437
  },
368
438
  body: Buffer.from(body),
369
- auth: {
370
- username: config.username,
371
- password: config.password,
372
- },
439
+ auth,
373
440
  });
374
441
  if (response.statusCode === 200 || response.statusCode === 201) {
375
442
  let addonId;
@@ -388,7 +455,8 @@ export async function createAddon(config, appType) {
388
455
  if (response.statusCode === 401) {
389
456
  return {
390
457
  success: false,
391
- error: 'Unauthorized. Check username and password.',
458
+ error: unauthorizedMessage(kind),
459
+ authExpired: true,
392
460
  };
393
461
  }
394
462
  if (response.statusCode === 409) {
@@ -422,6 +490,7 @@ export async function activateApp(executableId, config, _appType) {
422
490
  const body = JSON.stringify({
423
491
  executableId,
424
492
  });
493
+ const { auth, kind } = configAuth(config);
425
494
  try {
426
495
  const response = await makeRequest(url, {
427
496
  method: 'PUT',
@@ -430,10 +499,7 @@ export async function activateApp(executableId, config, _appType) {
430
499
  'Content-Length': String(Buffer.byteLength(body)),
431
500
  },
432
501
  body: Buffer.from(body),
433
- auth: {
434
- username: config.username,
435
- password: config.password,
436
- },
502
+ auth,
437
503
  });
438
504
  if (response.statusCode === 200) {
439
505
  return { success: true };
@@ -441,7 +507,8 @@ export async function activateApp(executableId, config, _appType) {
441
507
  if (response.statusCode === 401) {
442
508
  return {
443
509
  success: false,
444
- error: 'Unauthorized. Check username and password.',
510
+ error: unauthorizedMessage(kind),
511
+ authExpired: true,
445
512
  };
446
513
  }
447
514
  return {
@@ -459,4 +526,4 @@ export async function activateApp(executableId, config, _appType) {
459
526
  // =============================================================================
460
527
  // HELPER EXPORTS
461
528
  // =============================================================================
462
- export { createBasicAuth };
529
+ export { createBasicAuth, configAuth, unauthorizedMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.7",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "svc": "dist/cli.js"
@@ -24,6 +24,7 @@
24
24
  "ink": "^7.1.0",
25
25
  "ink-spinner": "^5.0.0",
26
26
  "meow": "^14.1.0",
27
+ "puppeteer-core": "^25.10.0",
27
28
  "react": "^19.2.7"
28
29
  },
29
30
  "devDependencies": {