usertold 1.9.1 → 1.9.3
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.
- package/package.json +1 -1
- package/usertold +188 -87
package/package.json
CHANGED
package/usertold
CHANGED
|
@@ -310,8 +310,8 @@ class HttpError extends CliError {
|
|
|
310
310
|
this.details = details;
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
|
-
async function requestJson(input) {
|
|
314
|
-
const response = await requestRaw(input);
|
|
313
|
+
async function requestJson(input, retryOptions) {
|
|
314
|
+
const response = await requestRaw(input, retryOptions);
|
|
315
315
|
if (!response.ok) {
|
|
316
316
|
throw new HttpError(response.status, extractErrorDetails(response));
|
|
317
317
|
}
|
|
@@ -371,7 +371,7 @@ async function requestJson(input) {
|
|
|
371
371
|
const arrayBuffer = await response.arrayBuffer();
|
|
372
372
|
return Buffer.from(arrayBuffer);
|
|
373
373
|
}
|
|
374
|
-
async function requestRaw(input) {
|
|
374
|
+
async function requestRaw(input, retryOptions) {
|
|
375
375
|
const method = input.method.toUpperCase();
|
|
376
376
|
const authMode = input.authMode ?? 'required';
|
|
377
377
|
const headers = {
|
|
@@ -395,21 +395,41 @@ async function requestRaw(input) {
|
|
|
395
395
|
}
|
|
396
396
|
const baseUrl = resolveBaseUrl(input.env);
|
|
397
397
|
const path = input.path.startsWith('/') ? input.path : `/${input.path}`;
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
398
|
+
const url = `${baseUrl}${path}`;
|
|
399
|
+
const retries = retryOptions?.retries ?? 0;
|
|
400
|
+
const initialDelayMs = retryOptions?.initialDelayMs ?? 500;
|
|
401
|
+
const maxDelayMs = retryOptions?.maxDelayMs ?? 4_000;
|
|
402
|
+
let delayMs = initialDelayMs;
|
|
403
|
+
let lastError;
|
|
404
|
+
for(let attempt = 0; attempt <= retries; attempt++){
|
|
405
|
+
try {
|
|
406
|
+
const response = await fetch(url, {
|
|
407
|
+
method,
|
|
408
|
+
headers,
|
|
409
|
+
body
|
|
410
|
+
});
|
|
411
|
+
const parsed = await toApiResponse(response);
|
|
412
|
+
if (attempt >= retries || !retryOptions?.shouldRetry?.({
|
|
413
|
+
attempt,
|
|
414
|
+
response: parsed
|
|
415
|
+
})) {
|
|
416
|
+
return parsed;
|
|
417
|
+
}
|
|
418
|
+
const retryAfterMs = parseRetryAfterMs(parsed.headers.get('Retry-After'));
|
|
419
|
+
await sleep$1(jitter$1(retryAfterMs ?? delayMs));
|
|
420
|
+
} catch (error) {
|
|
421
|
+
lastError = error;
|
|
422
|
+
if (attempt >= retries || !retryOptions?.shouldRetry?.({
|
|
423
|
+
attempt,
|
|
424
|
+
error
|
|
425
|
+
})) {
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
await sleep$1(jitter$1(delayMs));
|
|
429
|
+
}
|
|
430
|
+
delayMs = Math.min(maxDelayMs, delayMs * 2);
|
|
431
|
+
}
|
|
432
|
+
throw lastError ?? new Error('Request failed');
|
|
413
433
|
}
|
|
414
434
|
async function resolveToken(env, authMode) {
|
|
415
435
|
if (authMode === 'none') {
|
|
@@ -435,6 +455,9 @@ async function resolveToken(env, authMode) {
|
|
|
435
455
|
return config.token.accessToken;
|
|
436
456
|
}
|
|
437
457
|
function extractErrorDetails(response) {
|
|
458
|
+
if (isCloudflare1010Response(response)) {
|
|
459
|
+
return 'Cloudflare blocked this request (error 1010). Wait a few seconds and retry, or run pushes sequentially.';
|
|
460
|
+
}
|
|
438
461
|
if (response.json && typeof response.json === 'object' && response.json !== null) {
|
|
439
462
|
const body = response.json;
|
|
440
463
|
const maybeErrorCode = body.error_code;
|
|
@@ -472,6 +495,43 @@ function safeParseJson(text) {
|
|
|
472
495
|
return null;
|
|
473
496
|
}
|
|
474
497
|
}
|
|
498
|
+
async function toApiResponse(response) {
|
|
499
|
+
const text = await response.text();
|
|
500
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
501
|
+
const json = contentType.includes('application/json') && text.length > 0 ? safeParseJson(text) : null;
|
|
502
|
+
return {
|
|
503
|
+
status: response.status,
|
|
504
|
+
ok: response.ok,
|
|
505
|
+
headers: response.headers,
|
|
506
|
+
text,
|
|
507
|
+
json
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function parseRetryAfterMs(value) {
|
|
511
|
+
if (!value) return null;
|
|
512
|
+
const seconds = Number(value);
|
|
513
|
+
if (!Number.isNaN(seconds) && seconds >= 0) {
|
|
514
|
+
return seconds * 1000;
|
|
515
|
+
}
|
|
516
|
+
const dateMs = Date.parse(value);
|
|
517
|
+
if (Number.isNaN(dateMs)) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
return Math.max(0, dateMs - Date.now());
|
|
521
|
+
}
|
|
522
|
+
function jitter$1(ms) {
|
|
523
|
+
return ms * (0.7 + Math.random() * 0.6);
|
|
524
|
+
}
|
|
525
|
+
function sleep$1(ms) {
|
|
526
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
527
|
+
}
|
|
528
|
+
function isCloudflare1010Response(response) {
|
|
529
|
+
const text = response.text.toLowerCase();
|
|
530
|
+
if (text.includes('error code 1010') || text.includes('error code: 1010')) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
return text.includes('access denied') && text.includes("browser's signature");
|
|
534
|
+
}
|
|
475
535
|
|
|
476
536
|
/**
|
|
477
537
|
* Returns true when output should be JSON:
|
|
@@ -668,79 +728,98 @@ function generateState() {
|
|
|
668
728
|
function base64UrlEncode(buffer) {
|
|
669
729
|
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
670
730
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
res.writeHead(400, {
|
|
676
|
-
'Content-Type': 'text/html'
|
|
677
|
-
});
|
|
678
|
-
res.end(getErrorPage('invalid_request', 'Invalid request'));
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
682
|
-
if (url.pathname !== '/callback') {
|
|
683
|
-
res.writeHead(404, {
|
|
684
|
-
'Content-Type': 'text/html'
|
|
685
|
-
});
|
|
686
|
-
res.end(getErrorPage('not_found', 'Page not found'));
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
const code = url.searchParams.get('code');
|
|
690
|
-
const state = url.searchParams.get('state');
|
|
691
|
-
const error = url.searchParams.get('error');
|
|
692
|
-
const errorDescription = url.searchParams.get('error_description');
|
|
693
|
-
// Handle OAuth error response
|
|
694
|
-
if (error) {
|
|
695
|
-
res.writeHead(400, {
|
|
696
|
-
'Content-Type': 'text/html'
|
|
697
|
-
});
|
|
698
|
-
res.end(getErrorPage(error, errorDescription || undefined));
|
|
699
|
-
clearTimeout(timeout);
|
|
700
|
-
server.close();
|
|
701
|
-
reject(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ''}`));
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (!code) {
|
|
705
|
-
res.writeHead(400, {
|
|
706
|
-
'Content-Type': 'text/html'
|
|
707
|
-
});
|
|
708
|
-
res.end(getErrorPage('invalid_request', 'Missing authorization code'));
|
|
709
|
-
clearTimeout(timeout);
|
|
710
|
-
server.close();
|
|
711
|
-
reject(new Error('Missing authorization code'));
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
if (state !== expectedState) {
|
|
715
|
-
res.writeHead(400, {
|
|
716
|
-
'Content-Type': 'text/html'
|
|
717
|
-
});
|
|
718
|
-
res.end(getErrorPage('invalid_request', 'State parameter mismatch - possible CSRF attack'));
|
|
719
|
-
clearTimeout(timeout);
|
|
720
|
-
server.close();
|
|
721
|
-
reject(new Error('State mismatch'));
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
res.writeHead(200, {
|
|
731
|
+
function startAuthorizationCodeListener(port, expectedState) {
|
|
732
|
+
const server = http.createServer((req, res)=>{
|
|
733
|
+
if (!req.url) {
|
|
734
|
+
res.writeHead(400, {
|
|
725
735
|
'Content-Type': 'text/html'
|
|
726
736
|
});
|
|
727
|
-
res.end(
|
|
737
|
+
res.end(getErrorPage('invalid_request', 'Invalid request'));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
741
|
+
if (url.pathname !== '/callback') {
|
|
742
|
+
res.writeHead(404, {
|
|
743
|
+
'Content-Type': 'text/html'
|
|
744
|
+
});
|
|
745
|
+
res.end(getErrorPage('not_found', 'Page not found'));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const code = url.searchParams.get('code');
|
|
749
|
+
const state = url.searchParams.get('state');
|
|
750
|
+
const error = url.searchParams.get('error');
|
|
751
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
752
|
+
// Handle OAuth error response
|
|
753
|
+
if (error) {
|
|
754
|
+
res.writeHead(400, {
|
|
755
|
+
'Content-Type': 'text/html'
|
|
756
|
+
});
|
|
757
|
+
res.end(getErrorPage(error, errorDescription || undefined));
|
|
728
758
|
clearTimeout(timeout);
|
|
729
759
|
server.close();
|
|
730
|
-
|
|
731
|
-
|
|
760
|
+
rejectResult(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ''}`));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!code) {
|
|
764
|
+
res.writeHead(400, {
|
|
765
|
+
'Content-Type': 'text/html'
|
|
766
|
+
});
|
|
767
|
+
res.end(getErrorPage('invalid_request', 'Missing authorization code'));
|
|
768
|
+
clearTimeout(timeout);
|
|
769
|
+
server.close();
|
|
770
|
+
rejectResult(new Error('Missing authorization code'));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (state !== expectedState) {
|
|
774
|
+
res.writeHead(400, {
|
|
775
|
+
'Content-Type': 'text/html'
|
|
732
776
|
});
|
|
777
|
+
res.end(getErrorPage('invalid_request', 'State parameter mismatch - possible CSRF attack'));
|
|
778
|
+
clearTimeout(timeout);
|
|
779
|
+
server.close();
|
|
780
|
+
rejectResult(new Error('State mismatch'));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
res.writeHead(200, {
|
|
784
|
+
'Content-Type': 'text/html'
|
|
785
|
+
});
|
|
786
|
+
res.end(getSuccessPage());
|
|
787
|
+
clearTimeout(timeout);
|
|
788
|
+
server.close();
|
|
789
|
+
resolveResult({
|
|
790
|
+
code
|
|
733
791
|
});
|
|
734
|
-
|
|
735
|
-
|
|
792
|
+
});
|
|
793
|
+
let rejectResult = ()=>{};
|
|
794
|
+
let resolveResult = ()=>{};
|
|
795
|
+
const result = new Promise((resolve, reject)=>{
|
|
796
|
+
resolveResult = resolve;
|
|
797
|
+
rejectResult = reject;
|
|
798
|
+
});
|
|
799
|
+
const ready = new Promise((resolve, reject)=>{
|
|
800
|
+
const handleStartupError = (error)=>{
|
|
736
801
|
clearTimeout(timeout);
|
|
802
|
+
rejectResult(new Error(`Failed to start local callback server: ${error.message}`));
|
|
737
803
|
reject(new Error(`Failed to start local callback server: ${error.message}`));
|
|
804
|
+
};
|
|
805
|
+
server.once('error', handleStartupError);
|
|
806
|
+
server.listen(port, '127.0.0.1', ()=>{
|
|
807
|
+
server.off('error', handleStartupError);
|
|
808
|
+
server.on('error', (error)=>{
|
|
809
|
+
clearTimeout(timeout);
|
|
810
|
+
rejectResult(new Error(`Local callback server error: ${error.message}`));
|
|
811
|
+
});
|
|
812
|
+
resolve();
|
|
738
813
|
});
|
|
739
|
-
const timeout = setTimeout(()=>{
|
|
740
|
-
server.close();
|
|
741
|
-
reject(new Error('Login timed out waiting for authorization response'));
|
|
742
|
-
}, 5 * 60 * 1000);
|
|
743
814
|
});
|
|
815
|
+
const timeout = setTimeout(()=>{
|
|
816
|
+
server.close();
|
|
817
|
+
rejectResult(new Error('Login timed out waiting for authorization response'));
|
|
818
|
+
}, 5 * 60 * 1000);
|
|
819
|
+
return {
|
|
820
|
+
ready,
|
|
821
|
+
result
|
|
822
|
+
};
|
|
744
823
|
}
|
|
745
824
|
// =============================================================================
|
|
746
825
|
// CLI Auth Page Design Tokens
|
|
@@ -992,6 +1071,8 @@ async function handleLogin(parsed) {
|
|
|
992
1071
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
993
1072
|
authUrl.searchParams.set('state', state);
|
|
994
1073
|
authUrl.searchParams.set('access_type', 'offline');
|
|
1074
|
+
const authorizationCodeListener = startAuthorizationCodeListener(redirectPort, state);
|
|
1075
|
+
await authorizationCodeListener.ready;
|
|
995
1076
|
// Always print the URL so it works on headless/remote hosts
|
|
996
1077
|
console.log('\nOpen this URL in your browser to authenticate:\n');
|
|
997
1078
|
console.log(authUrl.toString());
|
|
@@ -1008,7 +1089,7 @@ async function handleLogin(parsed) {
|
|
|
1008
1089
|
console.log('(Could not open browser — use the URL above)');
|
|
1009
1090
|
}
|
|
1010
1091
|
}
|
|
1011
|
-
const { code } = await
|
|
1092
|
+
const { code } = await authorizationCodeListener.result;
|
|
1012
1093
|
console.log('Received authorization code. Exchanging for access token...');
|
|
1013
1094
|
const tokenResponse = await exchangeCodeForToken({
|
|
1014
1095
|
baseUrl,
|
|
@@ -9280,7 +9361,8 @@ function requestProjectContract(options) {
|
|
|
9280
9361
|
body: options.body,
|
|
9281
9362
|
authMode: options.authMode,
|
|
9282
9363
|
projectKey: options.projectKey,
|
|
9283
|
-
headers: options.headers
|
|
9364
|
+
headers: options.headers,
|
|
9365
|
+
retryOptions: options.retryOptions
|
|
9284
9366
|
};
|
|
9285
9367
|
return doRequestContract({
|
|
9286
9368
|
env: options.env
|
|
@@ -9347,7 +9429,7 @@ async function doRequestContract(defaults, key, options) {
|
|
|
9347
9429
|
authMode: options.authMode ?? defaults.authMode,
|
|
9348
9430
|
projectKey: options.projectKey ?? defaults.projectKey,
|
|
9349
9431
|
headers: mergeHeaders(defaults.headers, options.headers)
|
|
9350
|
-
});
|
|
9432
|
+
}, options.retryOptions);
|
|
9351
9433
|
}
|
|
9352
9434
|
function mergeHeaders(defaults, overrides) {
|
|
9353
9435
|
if (!defaults && !overrides) {
|
|
@@ -11050,6 +11132,12 @@ const FLAGS$a = {
|
|
|
11050
11132
|
],
|
|
11051
11133
|
'push-status': []
|
|
11052
11134
|
};
|
|
11135
|
+
const TASK_PUSH_RETRY_OPTIONS = {
|
|
11136
|
+
retries: 4,
|
|
11137
|
+
initialDelayMs: 500,
|
|
11138
|
+
maxDelayMs: 4_000,
|
|
11139
|
+
shouldRetry: ({ response, error })=>isRetryableTaskPushFailure(response, error)
|
|
11140
|
+
};
|
|
11053
11141
|
async function handleTaskCommand(subcommand, parsed) {
|
|
11054
11142
|
if (!subcommand || hasHelpFlag(parsed) || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
11055
11143
|
printTaskHelp();
|
|
@@ -11279,7 +11367,8 @@ async function handleTaskCommand(subcommand, parsed) {
|
|
|
11279
11367
|
sourceLabel: '<projectRef>',
|
|
11280
11368
|
pathParams: {
|
|
11281
11369
|
taskId: taskId
|
|
11282
|
-
}
|
|
11370
|
+
},
|
|
11371
|
+
retryOptions: TASK_PUSH_RETRY_OPTIONS
|
|
11283
11372
|
});
|
|
11284
11373
|
printOutput(data, parsed);
|
|
11285
11374
|
return;
|
|
@@ -11354,6 +11443,18 @@ Examples:
|
|
|
11354
11443
|
function printTaskHelp() {
|
|
11355
11444
|
console.log(TASK_HELP);
|
|
11356
11445
|
}
|
|
11446
|
+
function isRetryableTaskPushFailure(response, error) {
|
|
11447
|
+
if (response) {
|
|
11448
|
+
if (response.status === 429) {
|
|
11449
|
+
return true;
|
|
11450
|
+
}
|
|
11451
|
+
if (response.status >= 500 && response.status <= 504) {
|
|
11452
|
+
return true;
|
|
11453
|
+
}
|
|
11454
|
+
return response.status === 403 && isCloudflare1010Response(response);
|
|
11455
|
+
}
|
|
11456
|
+
return error instanceof Error;
|
|
11457
|
+
}
|
|
11357
11458
|
|
|
11358
11459
|
const FLAGS$9 = {
|
|
11359
11460
|
list: [],
|
|
@@ -14633,7 +14734,7 @@ function printExtractHelp() {
|
|
|
14633
14734
|
console.log(EXTRACT_HELP);
|
|
14634
14735
|
}
|
|
14635
14736
|
|
|
14636
|
-
const CLI_VERSION$1 = '1.9.
|
|
14737
|
+
const CLI_VERSION$1 = '1.9.3';
|
|
14637
14738
|
const GLOBAL_FLAGS = [
|
|
14638
14739
|
'env',
|
|
14639
14740
|
'json',
|
|
@@ -14929,7 +15030,7 @@ function printCompletionsHelp() {
|
|
|
14929
15030
|
console.log(COMPLETIONS_HELP);
|
|
14930
15031
|
}
|
|
14931
15032
|
|
|
14932
|
-
const CLI_VERSION = '1.9.
|
|
15033
|
+
const CLI_VERSION = '1.9.3';
|
|
14933
15034
|
function detectJsonMode() {
|
|
14934
15035
|
const argv = process$2.argv.slice(2);
|
|
14935
15036
|
if (argv.includes('--json')) return true;
|