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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/usertold +188 -87
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usertold",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "UserTold.ai CLI",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
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 response = await fetch(`${baseUrl}${path}`, {
399
- method,
400
- headers,
401
- body
402
- });
403
- const text = await response.text();
404
- const contentType = response.headers.get('content-type') ?? '';
405
- const json = contentType.includes('application/json') && text.length > 0 ? safeParseJson(text) : null;
406
- return {
407
- status: response.status,
408
- ok: response.ok,
409
- headers: response.headers,
410
- text,
411
- json
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
- async function waitForAuthorizationCode(port, expectedState) {
672
- return new Promise((resolve, reject)=>{
673
- const server = http.createServer((req, res)=>{
674
- if (!req.url) {
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(getSuccessPage());
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
- resolve({
731
- code
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
- server.listen(port, '127.0.0.1');
735
- server.on('error', (error)=>{
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 waitForAuthorizationCode(redirectPort, state);
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.1';
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.1';
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;