@cloudflare/workers-utils 0.36.0 → 0.37.0

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/dist/index.mjs CHANGED
@@ -2,10 +2,10 @@ export { MetricsRegistry } from './chunk-Z62CVNHJ.mjs';
2
2
  export { _forceColour, formatZodError } from './chunk-7AX24RFK.mjs';
3
3
  import { isRedirectedRawConfig, configFileName, formatConfigSnippet, getDurableObjectExports, getContainerNameToClassNameMap, parsePackageJSON, readFileSync, ParseError, parseJSON, APIError, dir, parseTOML, modify, applyEdits, format, dist_default, parseJSONC } from './chunk-V6JSHOVJ.mjs';
4
4
  export { APIError, ENVIRONMENT_TAG_PREFIX, INHERIT_SYMBOL, JSON_CONFIG_FORMATS, PATH_TO_DEPLOY_CONFIG, ParseError, SERVICE_TAG_PREFIX, configFileName, configFormat, experimental_readRawConfig, findWranglerConfig, formatConfigSnippet, getContainerDurableObjectClassNames, getContainerNameToClassNameMap, getDurableObjectExports, hasDurableObjectExports, indexLocation, isRedirectedConfig, parseByteSize, parseHumanDuration, parseJSON, parseJSONC, parseNonHyphenedUuid, parsePackageJSON, parseTOML, partitionExports, readFileSync, readFileSyncToBuffer, resolveContainerClassName, resolveWranglerConfigPath, searchLocation } from './chunk-V6JSHOVJ.mjs';
5
- import { assertNever } from './chunk-2I4Q5QJP.mjs';
6
- export { DEFAULT_COMPAT_DATE, assertNever, constructWranglerConfig, mapWorkerMetadataBindings } from './chunk-2I4Q5QJP.mjs';
5
+ import { assertNever } from './chunk-CDB2RWUW.mjs';
6
+ export { DEFAULT_COMPAT_DATE, assertNever, constructWranglerConfig, mapWorkerMetadataBindings } from './chunk-CDB2RWUW.mjs';
7
7
  export { NODEJS_COMPAT_DEFAULT_ON_DATE, NODEJS_COMPAT_V2_SWITCH_OVER_DATE, getTodaysCompatDate, isCompatDate, isNodejsCompatDefaultOn, resolveNodejsCompat, stripRedundantNodejsCompatFlags } from './chunk-C6O4ION7.mjs';
8
- import { getCloudflareEnv, getCloudflaredPathFromEnv, getCloudflareApiBaseUrl, getTraceHeader, getOutputFilePathFromEnv, getOutputFileDirectoryFromEnv, getCIOverrideName, getWranglerCacheDirFromEnv } from './chunk-XUGGRRBO.mjs';
8
+ import { getCloudflareEnv, getCloudflareApiBaseUrl, getTraceHeader, getCloudflaredPathFromEnv, getOutputFilePathFromEnv, getOutputFileDirectoryFromEnv, getCIOverrideName, getWranglerCacheDirFromEnv } from './chunk-XUGGRRBO.mjs';
9
9
  export { COMPLIANCE_REGION_CONFIG_PUBLIC, COMPLIANCE_REGION_CONFIG_UNKNOWN, getBrowserRenderingHeadfulFromEnv, getBuildConditionsFromEnv, getBuildPlatformFromEnv, getC3CommandFromEnv, getCIGeneratePreviewAlias, getCIMatchTag, getCIOverrideName, getCIOverrideNetworkModeHost, getCfFetchEnabledFromEnv, getCfFetchPathFromEnv, getCloudflareApiBaseUrl, getCloudflareApiEnvironmentFromEnv, getCloudflareComplianceRegion, getCloudflareEnv, getCloudflareIncludeProcessEnvFromEnv, getCloudflareLoadDevVarsFromDotEnv, getCloudflaredPathFromEnv, getComplianceRegionSubdomain, getD1ExtraLocationChoices, getDisableConfigWatching, getLocalExplorerEnabledFromEnv, getLocalObservabilityEnabledFromEnv, getNoSkillsUpdatePromptsFromEnv, getOpenNextDeployFromEnv, getOutputFileDirectoryFromEnv, getOutputFilePathFromEnv, getRegistryPath, getSanitizeLogs, getSubdomainMixedStateCheckDisabled, getTraceHeader, getWorkersCIBranchName, getWranglerCacheDirFromEnv, getWranglerHideBanner, getWranglerSendErrorReportsFromEnv, getWranglerSendMetricsFromEnv, isDoNotTrackEnabled } from './chunk-XUGGRRBO.mjs';
10
10
  import { dedent } from './chunk-MCNIGVAF.mjs';
11
11
  export { getBooleanEnvironmentVariableFactory, getDockerPath, getEnvironmentVariableFactory } from './chunk-3IYLA2JH.mjs';
@@ -22,11 +22,11 @@ import * as path from 'node:path';
22
22
  import path__default, { join, dirname, resolve, basename } from 'node:path';
23
23
  import { stripVTControlCharacters } from 'node:util';
24
24
  import chalk from 'chalk';
25
+ import { URLSearchParams as URLSearchParams$1 } from 'node:url';
26
+ import { Headers, FormData, Response, fetch } from 'undici';
25
27
  import { execFileSync, spawn } from 'node:child_process';
26
28
  import { createHash, randomBytes } from 'node:crypto';
27
29
  import { arch } from 'node:os';
28
- import { fetch, Headers, FormData, Response } from 'undici';
29
- import { URLSearchParams } from 'node:url';
30
30
  import * as timersPromises from 'node:timers/promises';
31
31
  import { setTimeout as setTimeout$1 } from 'node:timers/promises';
32
32
  import { assert as assert$1 } from 'node:console';
@@ -20117,6 +20117,12 @@ function validateContainerApp(envName, topLevelName, configPath, options = {}) {
20117
20117
  containerAppOptional.image_vars,
20118
20118
  "object"
20119
20119
  );
20120
+ validateContainerObservability(
20121
+ diagnostics,
20122
+ `${field}.observability`,
20123
+ containerAppOptional.observability,
20124
+ config2
20125
+ );
20120
20126
  validateOptionalProperty(
20121
20127
  diagnostics,
20122
20128
  field,
@@ -20160,6 +20166,7 @@ function validateContainerApp(envName, topLevelName, configPath, options = {}) {
20160
20166
  "image",
20161
20167
  "image_build_context",
20162
20168
  "image_vars",
20169
+ "observability",
20163
20170
  "class_name",
20164
20171
  "scheduling_policy",
20165
20172
  "instance_type",
@@ -22400,6 +22407,133 @@ var validateExports = /* @__PURE__ */ __name((diagnostics, field, value) => {
22400
22407
  }
22401
22408
  return valid;
22402
22409
  }, "validateExports");
22410
+ var CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MIN = 1;
22411
+ var CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MAX = 99;
22412
+ var CONTAINER_OBSERVABILITY_TARGET_INSTANCE_COUNT_MIN = 1;
22413
+ function isContainerObservabilityEnabled(observability) {
22414
+ return observability?.logs?.enabled === true || observability?.enabled === true;
22415
+ }
22416
+ __name(isContainerObservabilityEnabled, "isContainerObservabilityEnabled");
22417
+ function hasConflictingContainerObservabilityEnabledValues(observability) {
22418
+ return typeof observability.enabled === "boolean" && typeof observability.logs?.enabled === "boolean" && observability.enabled !== observability.logs.enabled;
22419
+ }
22420
+ __name(hasConflictingContainerObservabilityEnabledValues, "hasConflictingContainerObservabilityEnabledValues");
22421
+ var validateContainerObservability = /* @__PURE__ */ __name((diagnostics, field, value) => {
22422
+ if (value === void 0) {
22423
+ return true;
22424
+ }
22425
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
22426
+ diagnostics.errors.push(
22427
+ `"${field}" should be an object but got ${JSON.stringify(value)}.`
22428
+ );
22429
+ return false;
22430
+ }
22431
+ const val = value;
22432
+ let isValid = true;
22433
+ isValid = validateOptionalProperty(
22434
+ diagnostics,
22435
+ field,
22436
+ "enabled",
22437
+ val.enabled,
22438
+ "boolean"
22439
+ ) && isValid;
22440
+ if (val.logs !== void 0) {
22441
+ if (typeof val.logs !== "object" || val.logs === null || Array.isArray(val.logs)) {
22442
+ diagnostics.errors.push(
22443
+ `Expected "${field}.logs" to be of type object but got ${JSON.stringify(
22444
+ val.logs
22445
+ )}.`
22446
+ );
22447
+ isValid = false;
22448
+ } else {
22449
+ isValid = validateOptionalProperty(
22450
+ diagnostics,
22451
+ field,
22452
+ "logs.enabled",
22453
+ val.logs.enabled,
22454
+ "boolean"
22455
+ ) && isValid;
22456
+ isValid = validateAdditionalProperties(
22457
+ diagnostics,
22458
+ `${field}.logs`,
22459
+ Object.keys(val.logs),
22460
+ ["enabled"]
22461
+ ) && isValid;
22462
+ }
22463
+ }
22464
+ if (val.enabled === void 0 && val.target_instance_percentage === void 0 && val.target_instance_count === void 0 && (val.logs === void 0 || typeof val.logs === "object" && val.logs !== null && !Array.isArray(val.logs) && val.logs.enabled === void 0)) {
22465
+ isValid = validateAtLeastOnePropertyRequired(diagnostics, field, [
22466
+ {
22467
+ key: "enabled",
22468
+ value: val.enabled,
22469
+ type: "boolean"
22470
+ },
22471
+ {
22472
+ key: "logs.enabled",
22473
+ value: val.logs?.enabled,
22474
+ type: "boolean"
22475
+ }
22476
+ ]) && isValid;
22477
+ }
22478
+ isValid = validateOptionalProperty(
22479
+ diagnostics,
22480
+ field,
22481
+ "target_instance_percentage",
22482
+ val.target_instance_percentage,
22483
+ "number"
22484
+ ) && isValid;
22485
+ isValid = validateOptionalProperty(
22486
+ diagnostics,
22487
+ field,
22488
+ "target_instance_count",
22489
+ val.target_instance_count,
22490
+ "number"
22491
+ ) && isValid;
22492
+ isValid = validateAdditionalProperties(diagnostics, field, Object.keys(val), [
22493
+ "enabled",
22494
+ "logs",
22495
+ "target_instance_percentage",
22496
+ "target_instance_count"
22497
+ ]) && isValid;
22498
+ if (hasConflictingContainerObservabilityEnabledValues(val)) {
22499
+ diagnostics.errors.push(
22500
+ `"${field}.enabled" and "${field}.logs.enabled" cannot be set to different values.`
22501
+ );
22502
+ isValid = false;
22503
+ }
22504
+ if (val.target_instance_percentage !== void 0 && val.target_instance_count !== void 0) {
22505
+ diagnostics.errors.push(
22506
+ `"${field}.target_instance_percentage" and "${field}.target_instance_count" cannot both be set.`
22507
+ );
22508
+ isValid = false;
22509
+ }
22510
+ if (typeof val.target_instance_percentage === "number" && (!Number.isInteger(val.target_instance_percentage) || val.target_instance_percentage < CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MIN || val.target_instance_percentage > CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MAX)) {
22511
+ diagnostics.errors.push(
22512
+ `"${field}.target_instance_percentage" must be an integer between ${CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MIN} and ${CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MAX} inclusive.`
22513
+ );
22514
+ isValid = false;
22515
+ }
22516
+ if (typeof val.target_instance_count === "number" && (!Number.isInteger(val.target_instance_count) || val.target_instance_count < CONTAINER_OBSERVABILITY_TARGET_INSTANCE_COUNT_MIN)) {
22517
+ diagnostics.errors.push(
22518
+ `"${field}.target_instance_count" must be a positive integer.`
22519
+ );
22520
+ isValid = false;
22521
+ }
22522
+ const observabilityEnabled = isContainerObservabilityEnabled(val);
22523
+ if (val.target_instance_percentage !== void 0 && !observabilityEnabled) {
22524
+ diagnostics.errors.push(
22525
+ `"${field}.target_instance_percentage" requires "${field}.enabled" or "${field}.logs.enabled" to be true because container observability overrides root observability.`
22526
+ );
22527
+ isValid = false;
22528
+ }
22529
+ if (val.target_instance_count !== void 0 && !observabilityEnabled) {
22530
+ diagnostics.errors.push(
22531
+ `"${field}.target_instance_count" requires "${field}.enabled" or "${field}.logs.enabled" to be true because container observability overrides root observability.`
22532
+ );
22533
+ isValid = false;
22534
+ }
22535
+ return isValid;
22536
+ }, "validateContainerObservability");
22403
22537
  var validateObservability = /* @__PURE__ */ __name((diagnostics, field, value) => {
22404
22538
  if (value === void 0) {
22405
22539
  return true;
@@ -24783,1168 +24917,1310 @@ function getWranglerTmpDir(projectRoot, prefix, cleanup = true) {
24783
24917
  }
24784
24918
  __name(getWranglerTmpDir, "getWranglerTmpDir");
24785
24919
 
24786
- // src/cloudflared.ts
24787
- var import_command_exists = __toESM(require_command_exists2());
24788
- var UPDATE_SERVICE_URL = "https://update.argotunnel.com";
24789
- var CLOUDFLARED_VERSION_PATTERN = /^\d{4}\.\d+\.\d+$/;
24790
- function sha256Hex(buffer) {
24791
- return createHash("sha256").update(buffer).digest("hex");
24920
+ // src/cfetch/errors.ts
24921
+ function buildDetailedError(message, ...extra) {
24922
+ return new ParseError({
24923
+ text: message,
24924
+ notes: extra.map((text) => ({ text })),
24925
+ telemetryMessage: false
24926
+ });
24792
24927
  }
24793
- __name(sha256Hex, "sha256Hex");
24794
- function getGoArch() {
24795
- const nodeArch = arch();
24796
- switch (nodeArch) {
24797
- case "x64":
24798
- return "amd64";
24799
- case "arm64":
24800
- return "arm64";
24801
- case "arm":
24802
- return "arm";
24803
- default:
24804
- throw new UserError(
24805
- `Unsupported architecture for cloudflared: ${nodeArch}
24806
-
24807
- cloudflared supports: x64 (amd64), arm64, arm
24808
-
24809
- You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.
24810
- Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
24811
- { telemetryMessage: "tunnel cloudflared unsupported architecture" }
24812
- );
24928
+ __name(buildDetailedError, "buildDetailedError");
24929
+ function maybeThrowFriendlyError(error51) {
24930
+ if (error51.message === "workers.api.error.email_verification_required") {
24931
+ throw buildDetailedError(
24932
+ "Please verify your account's email address and try again.",
24933
+ "Check your email for a verification link, or login to https://dash.cloudflare.com and request a new one."
24934
+ );
24813
24935
  }
24814
24936
  }
24815
- __name(getGoArch, "getGoArch");
24816
- function getGoOS() {
24817
- switch (process.platform) {
24818
- case "darwin":
24819
- return "darwin";
24820
- case "linux":
24821
- return "linux";
24822
- case "win32":
24823
- return "windows";
24824
- default:
24825
- throw new UserError(
24826
- `Unsupported platform for cloudflared: ${process.platform}
24827
-
24828
- cloudflared supports: darwin (macOS), linux, win32 (Windows)
24937
+ __name(maybeThrowFriendlyError, "maybeThrowFriendlyError");
24829
24938
 
24830
- You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.
24831
- Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
24832
- { telemetryMessage: "tunnel cloudflared unsupported platform" }
24833
- );
24834
- }
24939
+ // src/cfetch/index.ts
24940
+ function logHeaders(headers, logger) {
24941
+ const clone2 = cloneHeaders(headers);
24942
+ clone2.delete("Authorization");
24943
+ logger.debugWithSanitization?.(
24944
+ "HEADERS:",
24945
+ JSON.stringify(Object.fromEntries(clone2), null, 2)
24946
+ );
24835
24947
  }
24836
- __name(getGoOS, "getGoOS");
24837
- var GITHUB_RELEASE_BASE = "https://github.com/cloudflare/cloudflared/releases/download";
24838
- function getAssetFilename(goOS, goArch) {
24839
- if (goOS === "windows") {
24840
- return `cloudflared-${goOS}-${goArch}.exe`;
24841
- }
24842
- if (goOS === "darwin") {
24843
- return `cloudflared-${goOS}-${goArch}.tgz`;
24948
+ __name(logHeaders, "logHeaders");
24949
+ async function performApiFetchBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
24950
+ assert2(credentials, "credentials are required for performApiFetch");
24951
+ const method = init.method ?? "GET";
24952
+ assert2(
24953
+ resource.startsWith("/"),
24954
+ `CF API fetch - resource path must start with a "/" but got "${resource}"`
24955
+ );
24956
+ const headers = cloneHeaders(new Headers(init.headers));
24957
+ addAuthorizationHeader(headers, credentials);
24958
+ headers.set("User-Agent", userAgent);
24959
+ maybeAddTraceHeader(headers);
24960
+ const queryString = queryParams ? `?${queryParams.toString()}` : "";
24961
+ logger.debug(
24962
+ `-- START CF API REQUEST: ${method} ${getCloudflareApiBaseUrl(complianceConfig)}${resource}`
24963
+ );
24964
+ logger.debugWithSanitization?.("QUERY STRING:", queryString);
24965
+ logHeaders(headers, logger);
24966
+ logger.debugWithSanitization?.("INIT:", JSON.stringify({ ...init }, null, 2));
24967
+ if (init.body instanceof FormData) {
24968
+ logger.debugWithSanitization?.(
24969
+ "BODY:",
24970
+ await new Response(init.body).text(),
24971
+ null,
24972
+ 2
24973
+ );
24844
24974
  }
24845
- return `cloudflared-${goOS}-${goArch}`;
24975
+ logger.debug("-- END CF API REQUEST");
24976
+ return await fetch(
24977
+ `${getCloudflareApiBaseUrl(complianceConfig)}${resource}${queryString}`,
24978
+ {
24979
+ method,
24980
+ ...init,
24981
+ headers,
24982
+ signal: abortSignal
24983
+ }
24984
+ );
24846
24985
  }
24847
- __name(getAssetFilename, "getAssetFilename");
24848
- async function queryUpdateService(goOS, goArch, options) {
24849
- const { logger } = options ?? {};
24850
- const url2 = new URL(UPDATE_SERVICE_URL);
24851
- url2.searchParams.set("os", goOS);
24852
- url2.searchParams.set("arch", goArch);
24853
- logger?.debug(`Checking for latest cloudflared: ${url2.toString()}`);
24854
- let response;
24855
- try {
24856
- response = await fetch(url2.toString(), {
24857
- headers: { "User-Agent": "wrangler" }
24858
- });
24859
- } catch (e2) {
24860
- logger?.debug(
24861
- `Failed to reach update service: ${e2 instanceof Error ? e2.message : String(e2)}`
24862
- );
24863
- return null;
24986
+ __name(performApiFetchBase, "performApiFetchBase");
24987
+ async function fetchInternalBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
24988
+ const method = init.method ?? "GET";
24989
+ const response = await performApiFetchBase(
24990
+ complianceConfig,
24991
+ resource,
24992
+ init,
24993
+ userAgent,
24994
+ logger,
24995
+ queryParams,
24996
+ abortSignal,
24997
+ credentials
24998
+ );
24999
+ const jsonText = await response.text();
25000
+ logger.debug(
25001
+ "-- START CF API RESPONSE:",
25002
+ response.statusText,
25003
+ response.status
25004
+ );
25005
+ logHeaders(response.headers, logger);
25006
+ logger.debugWithSanitization?.("RESPONSE:", jsonText);
25007
+ logger.debug("-- END CF API RESPONSE");
25008
+ const retryAfterMs = parseRetryAfterMs(response.headers);
25009
+ if (!jsonText && (response.status === 204 || response.status === 205)) {
25010
+ return {
25011
+ response: {
25012
+ result: {},
25013
+ success: true,
25014
+ errors: [],
25015
+ messages: []
25016
+ },
25017
+ status: response.status,
25018
+ retryAfterMs
25019
+ };
24864
25020
  }
24865
- if (!response.ok) {
24866
- logger?.debug(
24867
- `Update service returned ${response.status} for ${goOS}/${goArch}`
25021
+ if (isWAFBlockResponse(response.headers)) {
25022
+ throwWAFBlockError(
25023
+ response.headers,
25024
+ method,
25025
+ resource,
25026
+ response.status,
25027
+ response.statusText,
25028
+ retryAfterMs
24868
25029
  );
24869
- return null;
24870
25030
  }
24871
- let data;
24872
25031
  try {
24873
- data = await response.json();
24874
- } catch (e2) {
24875
- logger?.debug(
24876
- `Update service returned non-JSON response: ${e2 instanceof Error ? e2.message : String(e2)}`
24877
- );
24878
- return null;
25032
+ const json2 = parseJSON(jsonText);
25033
+ return { response: json2, status: response.status, retryAfterMs };
25034
+ } catch {
25035
+ const rayId = extractWAFBlockRayId(response.headers);
25036
+ throw new APIError({
25037
+ text: "Received a malformed response from the API",
25038
+ notes: [
25039
+ {
25040
+ text: truncate(jsonText, 100)
25041
+ },
25042
+ {
25043
+ text: `${method} ${resource} -> ${response.status} ${response.statusText}`
25044
+ },
25045
+ ...rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : []
25046
+ ],
25047
+ status: response.status,
25048
+ retryAfterMs,
25049
+ telemetryMessage: false
25050
+ });
24879
25051
  }
24880
- if (typeof data.version === "string" && !CLOUDFLARED_VERSION_PATTERN.test(data.version)) {
24881
- throw new Error(
24882
- `[cloudflared] Invalid cloudflared version returned by update service: ${data.version}`
25052
+ }
25053
+ __name(fetchInternalBase, "fetchInternalBase");
25054
+ async function fetchResultBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25055
+ const {
25056
+ response: json2,
25057
+ status,
25058
+ retryAfterMs
25059
+ } = await fetchInternalBase(
25060
+ complianceConfig,
25061
+ resource,
25062
+ init,
25063
+ userAgent,
25064
+ logger,
25065
+ queryParams,
25066
+ abortSignal,
25067
+ credentials
25068
+ );
25069
+ if (json2.success) {
25070
+ return json2.result;
25071
+ } else {
25072
+ throwFetchError(resource, json2, status, retryAfterMs);
25073
+ }
25074
+ }
25075
+ __name(fetchResultBase, "fetchResultBase");
25076
+ async function fetchListResultBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, credentials) {
25077
+ const results = [];
25078
+ let getMoreResults = true;
25079
+ let cursor;
25080
+ while (getMoreResults) {
25081
+ if (cursor) {
25082
+ queryParams = new URLSearchParams$1(queryParams);
25083
+ queryParams.set("cursor", cursor);
25084
+ }
25085
+ const {
25086
+ response: json2,
25087
+ status,
25088
+ retryAfterMs
25089
+ } = await fetchInternalBase(
25090
+ complianceConfig,
25091
+ resource,
25092
+ init,
25093
+ userAgent,
25094
+ logger,
25095
+ queryParams,
25096
+ void 0,
25097
+ credentials
24883
25098
  );
25099
+ if (json2.success) {
25100
+ results.push(...json2.result);
25101
+ if (hasCursor(json2.result_info)) {
25102
+ cursor = json2.result_info?.cursor;
25103
+ } else {
25104
+ getMoreResults = false;
25105
+ }
25106
+ } else {
25107
+ throwFetchError(resource, json2, status, retryAfterMs);
25108
+ }
24884
25109
  }
24885
- if (data.error || !data.url || !data.version) {
24886
- return data.version ? data : null;
25110
+ return results;
25111
+ }
25112
+ __name(fetchListResultBase, "fetchListResultBase");
25113
+ function truncate(text, maxLength) {
25114
+ const { length } = text;
25115
+ if (length <= maxLength) {
25116
+ return text;
24887
25117
  }
24888
- return data;
25118
+ return `${text.substring(0, maxLength)}... (length = ${length})`;
24889
25119
  }
24890
- __name(queryUpdateService, "queryUpdateService");
24891
- async function getLatestVersionInfo(options) {
24892
- const { logger } = options ?? {};
24893
- const goOS = getGoOS();
24894
- const goArch = getGoArch();
24895
- const primary = await queryUpdateService(goOS, goArch, { logger });
24896
- if (primary && primary.url && primary.version) {
24897
- return primary;
25120
+ __name(truncate, "truncate");
25121
+ function isWAFBlockResponse(headers) {
25122
+ return headers.get("cf-mitigated") === "challenge";
25123
+ }
25124
+ __name(isWAFBlockResponse, "isWAFBlockResponse");
25125
+ function parseRetryAfterValue(retryAfter) {
25126
+ if (!retryAfter) {
25127
+ return void 0;
24898
25128
  }
24899
- logger?.debug(
24900
- `Update worker had no result for ${goOS}/${goArch}, falling back to GitHub release URL`
24901
- );
24902
- const fallback = await queryUpdateService("linux", "amd64", { logger });
24903
- if (!fallback?.version) {
24904
- throw new UserError(
24905
- `[cloudflared] Failed to determine the latest cloudflared version.
24906
-
24907
- The update service did not return results for ${goOS}/${goArch},
24908
- and the fallback query also failed.
24909
-
24910
- You can manually install cloudflared from:
24911
- https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
24912
- { telemetryMessage: "tunnel cloudflared version lookup failed" }
24913
- );
25129
+ if (/^\d+$/.test(retryAfter.trim())) {
25130
+ return Number(retryAfter) * 1e3;
24914
25131
  }
24915
- const version2 = fallback.version;
24916
- const filename = getAssetFilename(goOS, goArch);
24917
- const url2 = `${GITHUB_RELEASE_BASE}/${version2}/${filename}`;
24918
- const compressed = filename.endsWith(".tgz");
24919
- return {
24920
- url: url2,
24921
- version: version2,
24922
- checksum: "",
24923
- // no checksum available for fallback URLs
24924
- compressed,
24925
- shouldUpdate: true,
24926
- userMessage: "",
24927
- error: ""
24928
- };
25132
+ const retryAfterDate = new Date(retryAfter);
25133
+ if (!Number.isNaN(retryAfterDate.getTime())) {
25134
+ return Math.max(0, retryAfterDate.getTime() - Date.now());
25135
+ }
25136
+ return void 0;
24929
25137
  }
24930
- __name(getLatestVersionInfo, "getLatestVersionInfo");
24931
- function getCacheDir(version2) {
24932
- return join(getGlobalConfigPath(), "cloudflared", version2);
25138
+ __name(parseRetryAfterValue, "parseRetryAfterValue");
25139
+ function parseRetryAfterMs(headers) {
25140
+ return parseRetryAfterValue(headers.get("Retry-After"));
24933
25141
  }
24934
- __name(getCacheDir, "getCacheDir");
24935
- function getCloudflaredBinPath(version2) {
24936
- const binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
24937
- return join(getCacheDir(version2), binName);
25142
+ __name(parseRetryAfterMs, "parseRetryAfterMs");
25143
+ function extractWAFBlockRayId(headers) {
25144
+ return headers.get("cf-ray") ?? void 0;
24938
25145
  }
24939
- __name(getCloudflaredBinPath, "getCloudflaredBinPath");
24940
- function isBinaryExecutable(binPath) {
24941
- try {
24942
- accessSync(binPath, constants.X_OK);
24943
- return true;
24944
- } catch {
24945
- return false;
24946
- }
25146
+ __name(extractWAFBlockRayId, "extractWAFBlockRayId");
25147
+ function extractAccountTag(resource) {
25148
+ const re = new RegExp("/accounts/([a-zA-Z0-9]+)/?");
25149
+ const matches = re.exec(resource);
25150
+ return matches?.[1];
24947
25151
  }
24948
- __name(isBinaryExecutable, "isBinaryExecutable");
24949
- function validateBinary(binPath, options) {
24950
- const { logger } = options ?? {};
24951
- try {
24952
- const output = execFileSync(binPath, ["--version"], {
24953
- stdio: ["pipe", "pipe", "pipe"],
24954
- timeout: 1e4,
24955
- encoding: "utf8"
24956
- }).trim();
24957
- logger?.debug(`cloudflared version: ${output}`);
24958
- } catch {
24959
- let errorMessage = `[cloudflared] Failed to validate cloudflared binary at ${binPath}
25152
+ __name(extractAccountTag, "extractAccountTag");
25153
+ function hasMorePages(result_info) {
25154
+ const page = result_info?.page;
25155
+ const per_page = result_info?.per_page;
25156
+ const total = result_info?.total_count;
25157
+ return page !== void 0 && per_page !== void 0 && total !== void 0 && page * per_page < total;
25158
+ }
25159
+ __name(hasMorePages, "hasMorePages");
25160
+ function renderError(err, level = 0) {
25161
+ const indent = " ".repeat(level);
25162
+ const message = err.message ?? "";
25163
+ const chainedMessages = "error_chain" in err ? err.error_chain?.map(
25164
+ (chainedError) => `
24960
25165
 
24961
- `;
24962
- errorMessage += `This usually means:
24963
- `;
24964
- errorMessage += ` - The binary is corrupted or incomplete
24965
- `;
24966
- errorMessage += ` - You're missing required system libraries
24967
- `;
24968
- if (process.platform === "linux") {
24969
- errorMessage += `
24970
- On Linux, make sure you have the required dependencies:
24971
- `;
24972
- errorMessage += ` - glibc (GNU C Library)
24973
- `;
24974
- errorMessage += ` - For Debian/Ubuntu: sudo apt-get install libc6
24975
- `;
24976
- }
24977
- const cacheDir = join(getGlobalConfigPath(), "cloudflared");
24978
- errorMessage += `
24979
- You can try:
24980
- `;
24981
- errorMessage += ` 1. Deleting the cache directory: rm -rf ${cacheDir}
24982
- `;
24983
- errorMessage += ` 2. Running the command again to re-download
24984
- `;
24985
- errorMessage += ` 3. Manually installing cloudflared: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/
24986
- `;
24987
- errorMessage += ` 4. Setting CLOUDFLARED_PATH to point to your cloudflared binary`;
24988
- throw new UserError(errorMessage, {
24989
- telemetryMessage: "tunnel cloudflared validation failed"
24990
- });
24991
- }
25166
+ ${indent}- ${renderError(chainedError, level + 1)}`
25167
+ ).join("\n") ?? "" : "";
25168
+ return (err.code ? `${message} [code: ${err.code}]` : message) + (err.documentation_url ? `
25169
+ ${indent}To learn more about this error, visit: ${err.documentation_url}` : "") + chainedMessages;
24992
25170
  }
24993
- __name(validateBinary, "validateBinary");
24994
- function redactCloudflaredArgsForLogging(args) {
24995
- const redacted = [...args];
24996
- for (let i = 0; i < redacted.length; i++) {
24997
- const arg = redacted[i];
24998
- if (arg === "--token" && i + 1 < redacted.length) {
24999
- redacted[i + 1] = "[REDACTED]";
25000
- }
25001
- if (arg.startsWith("--token=")) {
25002
- redacted[i] = "--token=[REDACTED]";
25171
+ __name(renderError, "renderError");
25172
+ function addAuthorizationHeader(headers, auth, overrideExisting = false) {
25173
+ if (!headers.has("Authorization") || overrideExisting) {
25174
+ if ("apiToken" in auth) {
25175
+ const authorizationHeader = `Bearer ${auth.apiToken}`;
25176
+ validateAuthorizationHeaderValue(authorizationHeader);
25177
+ headers.set("Authorization", authorizationHeader);
25178
+ } else {
25179
+ headers.set("X-Auth-Key", auth.authKey);
25180
+ headers.set("X-Auth-Email", auth.authEmail);
25003
25181
  }
25004
25182
  }
25005
- return redacted;
25006
25183
  }
25007
- __name(redactCloudflaredArgsForLogging, "redactCloudflaredArgsForLogging");
25008
- function tryGetCloudflaredFromPath(options) {
25009
- const { logger } = options ?? {};
25010
- if (!(0, import_command_exists.sync)("cloudflared")) {
25011
- return null;
25012
- }
25013
- try {
25014
- validateBinary("cloudflared", { logger });
25015
- return "cloudflared";
25016
- } catch (e2) {
25017
- logger?.debug("cloudflared found in PATH but failed validation", e2);
25018
- return null;
25184
+ __name(addAuthorizationHeader, "addAuthorizationHeader");
25185
+ function validateAuthorizationHeaderValue(value) {
25186
+ for (const character of value) {
25187
+ const codePoint = character.codePointAt(0);
25188
+ if (codePoint === void 0 || codePoint > 255) {
25189
+ throw new UserError(
25190
+ `The configured Cloudflare API token contains a character that cannot be used in an HTTP Authorization header: ${formatAuthorizationHeaderCharacter(character, codePoint)}. Recreate or copy the token again, making sure it does not include characters such as ellipses.`,
25191
+ {
25192
+ telemetryMessage: "cfetch auth invalid authorization header"
25193
+ }
25194
+ );
25195
+ }
25019
25196
  }
25020
25197
  }
25021
- __name(tryGetCloudflaredFromPath, "tryGetCloudflaredFromPath");
25022
- function getInstalledVersion(binPath) {
25023
- try {
25024
- const output = execFileSync(binPath, ["--version"], {
25025
- stdio: ["pipe", "pipe", "pipe"],
25026
- timeout: 1e4,
25027
- encoding: "utf8"
25028
- });
25029
- const match = output.match(/(\d+\.\d+\.\d+)/);
25030
- return match ? match[1] : null;
25031
- } catch {
25032
- return null;
25198
+ __name(validateAuthorizationHeaderValue, "validateAuthorizationHeaderValue");
25199
+ function formatAuthorizationHeaderCharacter(character, codePoint) {
25200
+ if (codePoint === void 0) {
25201
+ return '"\\u{unknown}"';
25033
25202
  }
25203
+ const codePointLabel = `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`;
25204
+ const characterLabel = isPrintableCharacter(character) ? `"${character}"` : `"${escapeCharacter(character)}"`;
25205
+ return `${characterLabel} (${codePointLabel})`;
25034
25206
  }
25035
- __name(getInstalledVersion, "getInstalledVersion");
25036
- function isVersionOutdated(installed, latest) {
25037
- const parse3 = /* @__PURE__ */ __name((v) => v.split(".").map(Number), "parse");
25038
- const [iYear, iMonth, iPatch] = parse3(installed);
25039
- const [lYear, lMonth, lPatch] = parse3(latest);
25040
- if (iYear !== lYear) {
25041
- return iYear < lYear;
25042
- }
25043
- if (iMonth !== lMonth) {
25044
- return iMonth < lMonth;
25045
- }
25046
- return iPatch < lPatch;
25207
+ __name(formatAuthorizationHeaderCharacter, "formatAuthorizationHeaderCharacter");
25208
+ function isPrintableCharacter(character) {
25209
+ return !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character);
25047
25210
  }
25048
- __name(isVersionOutdated, "isVersionOutdated");
25049
- async function warnIfOutdated(binPath, options) {
25050
- const { logger } = options ?? {};
25051
- try {
25052
- const installed = getInstalledVersion(binPath);
25053
- if (!installed) {
25054
- return;
25055
- }
25056
- const latest = await queryUpdateService(getGoOS(), getGoArch(), { logger }) ?? await queryUpdateService("linux", "amd64", { logger });
25057
- const latestVersion = latest?.version;
25058
- if (!latestVersion) {
25059
- return;
25060
- }
25061
- if (isVersionOutdated(installed, latestVersion)) {
25062
- logger?.warn(
25063
- `Your cloudflared (${installed}) is outdated. Latest version is ${latestVersion}.
25064
- Update: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`
25065
- );
25211
+ __name(isPrintableCharacter, "isPrintableCharacter");
25212
+ function escapeCharacter(character) {
25213
+ return Array.from(character).map((c) => {
25214
+ const codePoint = c.codePointAt(0);
25215
+ if (codePoint === void 0) {
25216
+ return "";
25066
25217
  }
25067
- } catch (e2) {
25068
- logger?.debug("Failed to check cloudflared version", e2);
25069
- }
25218
+ return codePoint <= 65535 ? `\\u${codePoint.toString(16).toUpperCase().padStart(4, "0")}` : `\\u{${codePoint.toString(16).toUpperCase()}}`;
25219
+ }).join("");
25070
25220
  }
25071
- __name(warnIfOutdated, "warnIfOutdated");
25072
- function writeFileAtomic(filePath, contents) {
25073
- const dir2 = dirname(filePath);
25074
- const tmpPath = join(
25075
- dir2,
25076
- `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
25077
- );
25078
- try {
25079
- writeFileSync(tmpPath, contents);
25080
- renameSync(tmpPath, filePath);
25081
- } finally {
25082
- try {
25083
- if (existsSync(tmpPath)) {
25084
- unlinkSync(tmpPath);
25085
- }
25086
- } catch {
25221
+ __name(escapeCharacter, "escapeCharacter");
25222
+ function throwFetchError(resource, response, status, retryAfterMs) {
25223
+ const errors = response.errors ?? [];
25224
+ for (const error52 of errors) {
25225
+ maybeThrowFriendlyError(error52);
25226
+ }
25227
+ const notes = [
25228
+ ...errors.map((err) => ({ text: renderError(err) })),
25229
+ ...response.messages?.map((msg) => ({
25230
+ text: typeof msg === "string" ? msg : msg.message ?? String(msg)
25231
+ })) ?? []
25232
+ ];
25233
+ if (notes.length === 0) {
25234
+ const raw = response;
25235
+ const fallbackMessage = typeof raw.error === "string" ? `${raw.error}${raw.code ? ` [code: ${raw.code}]` : ""}` : void 0;
25236
+ if (fallbackMessage) {
25237
+ notes.push({ text: fallbackMessage });
25087
25238
  }
25088
25239
  }
25089
- }
25090
- __name(writeFileAtomic, "writeFileAtomic");
25091
- async function downloadCloudflared(versionInfo, binPath, options) {
25092
- const { logger } = options ?? {};
25093
- const { url: url2, version: version2, checksum, compressed } = versionInfo;
25094
- logger?.log(`Downloading cloudflared ${version2}...`);
25095
- logger?.debug(`Download URL: ${url2}`);
25096
- const cacheDir = dirname(binPath);
25097
- mkdirSync(cacheDir, { recursive: true });
25098
- let response;
25099
- try {
25100
- response = await fetch(url2, {
25101
- headers: { "User-Agent": "wrangler" }
25240
+ if (retryAfterMs !== void 0) {
25241
+ notes.push({
25242
+ text: `The API responded with a "Retry-After" header indicating you should wait ${Math.ceil(retryAfterMs / 1e3)} second(s) before retrying.`
25102
25243
  });
25103
- } catch (e2) {
25104
- throw new UserError(
25105
- `[cloudflared] Failed to download cloudflared from ${url2}
25106
-
25107
- Network error: ${e2 instanceof Error ? e2.message : String(e2)}
25108
-
25109
- Please check your internet connection and try again.
25110
- If you're behind a proxy, make sure it's configured correctly.`,
25111
- { telemetryMessage: "tunnel cloudflared download network failed" }
25112
- );
25113
25244
  }
25114
- if (!response.ok) {
25115
- throw new UserError(
25116
- `[cloudflared] Failed to download cloudflared from ${url2}
25117
-
25118
- HTTP ${response.status}: ${response.statusText}
25119
-
25120
- You can manually download cloudflared from:
25121
- https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25122
- { telemetryMessage: "tunnel cloudflared download response failed" }
25123
- );
25245
+ const error51 = new APIError({
25246
+ text: `A request to the Cloudflare API (${resource}) failed.`,
25247
+ notes,
25248
+ status,
25249
+ // hoist the parsed `Retry-After` header (if any) so consumers such as
25250
+ // `retryOnAPIFailure()` can back off for the amount of time the API
25251
+ // asked us to wait, e.g. when rate limited (HTTP 429).
25252
+ retryAfterMs,
25253
+ telemetryMessage: false
25254
+ });
25255
+ const code = errors[0]?.code;
25256
+ if (code) {
25257
+ error51.code = code;
25124
25258
  }
25125
- try {
25126
- if (compressed) {
25127
- await downloadAndExtractTarball(response, checksum, binPath, cacheDir);
25128
- } else {
25129
- await downloadBinary(response, checksum, binPath);
25130
- }
25131
- } catch (e2) {
25132
- try {
25133
- if (existsSync(binPath)) {
25134
- unlinkSync(binPath);
25259
+ const meta3 = errors[0]?.meta;
25260
+ if (meta3) {
25261
+ error51.meta = meta3;
25262
+ }
25263
+ error51.accountTag = extractAccountTag(resource);
25264
+ throw error51;
25265
+ }
25266
+ __name(throwFetchError, "throwFetchError");
25267
+ function throwWAFBlockError(headers, method, resource, status, statusText, retryAfterMs) {
25268
+ const rayId = extractWAFBlockRayId(headers);
25269
+ throw new APIError({
25270
+ text: "The Cloudflare API responded with a WAF block page instead of the expected JSON response",
25271
+ notes: [
25272
+ {
25273
+ text: "Cloudflare's firewall (WAF) blocked this API request. This is usually a false positive."
25274
+ },
25275
+ ...rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : [],
25276
+ {
25277
+ text: rayId ? "If the issue persists, please open a Cloudflare Support ticket and include the Ray ID above." : "If the issue persists, please open a Cloudflare Support ticket. You can find the Cloudflare Ray ID on the block page in your browser."
25278
+ },
25279
+ {
25280
+ text: `${method} ${resource} -> ${status} ${statusText}`
25135
25281
  }
25136
- } catch {
25137
- }
25138
- if (e2 instanceof UserError) {
25139
- throw e2;
25140
- }
25141
- throw new UserError(
25142
- `[cloudflared] Failed to save cloudflared binary
25143
-
25144
- Error: ${e2 instanceof Error ? e2.message : String(e2)}
25145
-
25146
- Please ensure you have write permissions to: ${cacheDir}`,
25147
- { telemetryMessage: "tunnel cloudflared save failed" }
25282
+ ],
25283
+ status,
25284
+ retryAfterMs,
25285
+ telemetryMessage: false
25286
+ });
25287
+ }
25288
+ __name(throwWAFBlockError, "throwWAFBlockError");
25289
+ async function fetchKVGetValueBase(complianceConfig, accountId, namespaceId, key, userAgent, logger, credentials) {
25290
+ const headers = new Headers();
25291
+ addAuthorizationHeader(headers, credentials);
25292
+ headers.set("User-Agent", userAgent);
25293
+ maybeAddTraceHeader(headers);
25294
+ const resource = `${getCloudflareApiBaseUrl(complianceConfig)}/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`;
25295
+ logger.debug(`-- START CF API REQUEST: GET ${resource}`);
25296
+ logger.debug("-- END CF API REQUEST");
25297
+ const response = await fetch(resource, {
25298
+ method: "GET",
25299
+ headers
25300
+ });
25301
+ if (response.ok) {
25302
+ return await response.arrayBuffer();
25303
+ } else {
25304
+ throw new Error(
25305
+ `Failed to fetch ${resource} - ${response.status}: ${response.statusText}`
25148
25306
  );
25149
25307
  }
25150
- if (process.platform !== "win32") {
25151
- chmodSync(binPath, 493);
25308
+ }
25309
+ __name(fetchKVGetValueBase, "fetchKVGetValueBase");
25310
+ function hasCursor(result_info) {
25311
+ const cursor = result_info?.cursor;
25312
+ return cursor !== void 0 && cursor !== null && cursor !== "";
25313
+ }
25314
+ __name(hasCursor, "hasCursor");
25315
+ function maybeAddTraceHeader(headers) {
25316
+ const traceHeader = getTraceHeader();
25317
+ if (traceHeader) {
25318
+ headers.set("Cf-Trace-Id", traceHeader);
25152
25319
  }
25153
- logger?.log(`cloudflared ${version2} installed`);
25154
- logger?.debug(`Binary location: ${binPath}`);
25155
25320
  }
25156
- __name(downloadCloudflared, "downloadCloudflared");
25157
- async function downloadAndExtractTarball(response, expectedChecksum, binPath, cacheDir) {
25158
- const tempTarPath = join(cacheDir, "cloudflared.tgz");
25159
- const buffer = Buffer.from(await response.arrayBuffer());
25160
- writeFileSync(tempTarPath, buffer);
25161
- try {
25162
- execFileSync("tar", ["-xzf", tempTarPath, "-C", cacheDir], {
25163
- stdio: "ignore"
25164
- });
25165
- const extractedPath = join(cacheDir, "cloudflared");
25166
- if (extractedPath !== binPath && existsSync(extractedPath)) {
25167
- renameSync(extractedPath, binPath);
25168
- }
25169
- if (expectedChecksum) {
25170
- const extractedBinary = readFileSync$1(binPath);
25171
- const actualSha256 = sha256Hex(extractedBinary);
25172
- if (actualSha256 !== expectedChecksum) {
25173
- throw new UserError(
25174
- `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.
25321
+ __name(maybeAddTraceHeader, "maybeAddTraceHeader");
25322
+ function cloneHeaders(headers) {
25323
+ return new Headers(headers);
25324
+ }
25325
+ __name(cloneHeaders, "cloneHeaders");
25175
25326
 
25176
- Expected: ${expectedChecksum}
25177
- Actual: ${actualSha256}`,
25178
- {
25179
- telemetryMessage: "tunnel cloudflared extracted binary checksum mismatch"
25180
- }
25181
- );
25182
- }
25183
- }
25184
- } finally {
25185
- try {
25186
- if (existsSync(tempTarPath)) {
25187
- unlinkSync(tempTarPath);
25188
- }
25189
- } catch {
25190
- }
25191
- }
25327
+ // src/cloudflared.ts
25328
+ var import_command_exists = __toESM(require_command_exists2());
25329
+ var UPDATE_SERVICE_URL = "https://update.argotunnel.com";
25330
+ var CLOUDFLARED_VERSION_PATTERN = /^\d{4}\.\d+\.\d+$/;
25331
+ function sha256Hex(buffer) {
25332
+ return createHash("sha256").update(buffer).digest("hex");
25192
25333
  }
25193
- __name(downloadAndExtractTarball, "downloadAndExtractTarball");
25194
- async function downloadBinary(response, expectedChecksum, binPath) {
25195
- const buffer = Buffer.from(await response.arrayBuffer());
25196
- if (expectedChecksum) {
25197
- const actualSha256 = sha256Hex(buffer);
25198
- if (actualSha256 !== expectedChecksum) {
25334
+ __name(sha256Hex, "sha256Hex");
25335
+ function getGoArch() {
25336
+ const nodeArch = arch();
25337
+ switch (nodeArch) {
25338
+ case "x64":
25339
+ return "amd64";
25340
+ case "arm64":
25341
+ return "arm64";
25342
+ case "arm":
25343
+ return "arm";
25344
+ default:
25199
25345
  throw new UserError(
25200
- `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.
25346
+ `Unsupported architecture for cloudflared: ${nodeArch}
25201
25347
 
25202
- Expected: ${expectedChecksum}
25203
- Actual: ${actualSha256}`,
25204
- { telemetryMessage: "tunnel cloudflared binary checksum mismatch" }
25348
+ cloudflared supports: x64 (amd64), arm64, arm
25349
+
25350
+ You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.
25351
+ Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25352
+ { telemetryMessage: "tunnel cloudflared unsupported architecture" }
25205
25353
  );
25206
- }
25207
25354
  }
25208
- writeFileAtomic(binPath, buffer);
25209
25355
  }
25210
- __name(downloadBinary, "downloadBinary");
25211
- async function getCloudflaredPath(options) {
25212
- const logger = options?.logger;
25213
- const envPath = getCloudflaredPathFromEnv();
25214
- if (envPath) {
25215
- if (!existsSync(envPath)) {
25356
+ __name(getGoArch, "getGoArch");
25357
+ function getGoOS() {
25358
+ switch (process.platform) {
25359
+ case "darwin":
25360
+ return "darwin";
25361
+ case "linux":
25362
+ return "linux";
25363
+ case "win32":
25364
+ return "windows";
25365
+ default:
25216
25366
  throw new UserError(
25217
- `CLOUDFLARED_PATH is set to "${envPath}" but the file does not exist.
25367
+ `Unsupported platform for cloudflared: ${process.platform}
25218
25368
 
25219
- Please ensure the path points to a valid cloudflared binary.`,
25220
- { telemetryMessage: "tunnel cloudflared env path missing" }
25369
+ cloudflared supports: darwin (macOS), linux, win32 (Windows)
25370
+
25371
+ You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.
25372
+ Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25373
+ { telemetryMessage: "tunnel cloudflared unsupported platform" }
25221
25374
  );
25222
- }
25223
- logger?.debug(`Using cloudflared from CLOUDFLARED_PATH: ${envPath}`);
25224
- return envPath;
25225
25375
  }
25226
- const pathBin = tryGetCloudflaredFromPath({ logger });
25227
- if (pathBin) {
25228
- logger?.debug("Using cloudflared from PATH");
25229
- if (!options?.skipVersionCheck) {
25230
- await warnIfOutdated(pathBin, { logger });
25231
- }
25232
- return pathBin;
25376
+ }
25377
+ __name(getGoOS, "getGoOS");
25378
+ var GITHUB_RELEASE_BASE = "https://github.com/cloudflare/cloudflared/releases/download";
25379
+ function getAssetFilename(goOS, goArch) {
25380
+ if (goOS === "windows") {
25381
+ return `cloudflared-${goOS}-${goArch}.exe`;
25233
25382
  }
25234
- const versionInfo = await getLatestVersionInfo({ logger });
25235
- const binPath = getCloudflaredBinPath(versionInfo.version);
25236
- let needsDownload = !isBinaryExecutable(binPath);
25237
- if (!needsDownload) {
25238
- try {
25239
- validateBinary(binPath, { logger });
25240
- logger?.debug(
25241
- `Using cached cloudflared ${versionInfo.version}: ${binPath}`
25242
- );
25243
- return binPath;
25244
- } catch (e2) {
25245
- logger?.debug("Cached cloudflared failed validation; re-downloading", e2);
25246
- needsDownload = true;
25247
- }
25383
+ if (goOS === "darwin") {
25384
+ return `cloudflared-${goOS}-${goArch}.tgz`;
25248
25385
  }
25249
- const shouldDownload = await options?.confirmDownload?.(
25250
- `cloudflared (${versionInfo.version}) is needed but not installed. Download to ${binPath}?`
25251
- ) ?? true;
25252
- if (!shouldDownload) {
25253
- throw new UserError(
25254
- `cloudflared is required to run this command.
25255
-
25256
- You can install it manually from:
25257
- https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/
25258
-
25259
- Then either add it to your PATH or set CLOUDFLARED_PATH.`,
25260
- { telemetryMessage: "tunnel cloudflared download declined" }
25386
+ return `cloudflared-${goOS}-${goArch}`;
25387
+ }
25388
+ __name(getAssetFilename, "getAssetFilename");
25389
+ async function queryUpdateService(goOS, goArch, options) {
25390
+ const { logger } = options ?? {};
25391
+ const url2 = new URL(UPDATE_SERVICE_URL);
25392
+ url2.searchParams.set("os", goOS);
25393
+ url2.searchParams.set("arch", goArch);
25394
+ logger?.debug(`Checking for latest cloudflared: ${url2.toString()}`);
25395
+ let response;
25396
+ try {
25397
+ response = await fetch(url2.toString(), {
25398
+ headers: { "User-Agent": "wrangler" }
25399
+ });
25400
+ } catch (e2) {
25401
+ logger?.debug(
25402
+ `Failed to reach update service: ${e2 instanceof Error ? e2.message : String(e2)}`
25261
25403
  );
25404
+ return null;
25262
25405
  }
25263
- if (existsSync(binPath)) {
25264
- const cacheDir = removeCloudflaredCache(versionInfo.version);
25265
- if (cacheDir) {
25266
- logger?.log(`Removed cloudflared cache: ${cacheDir}`);
25267
- }
25406
+ if (!response.ok) {
25407
+ logger?.debug(
25408
+ `Update service returned ${response.status} for ${goOS}/${goArch}`
25409
+ );
25410
+ return null;
25268
25411
  }
25269
- await downloadCloudflared(versionInfo, binPath, { logger });
25270
- validateBinary(binPath, { logger });
25271
- return binPath;
25412
+ let data;
25413
+ try {
25414
+ data = await response.json();
25415
+ } catch (e2) {
25416
+ logger?.debug(
25417
+ `Update service returned non-JSON response: ${e2 instanceof Error ? e2.message : String(e2)}`
25418
+ );
25419
+ return null;
25420
+ }
25421
+ if (typeof data.version === "string" && !CLOUDFLARED_VERSION_PATTERN.test(data.version)) {
25422
+ throw new Error(
25423
+ `[cloudflared] Invalid cloudflared version returned by update service: ${data.version}`
25424
+ );
25425
+ }
25426
+ if (data.error || !data.url || !data.version) {
25427
+ return data.version ? data : null;
25428
+ }
25429
+ return data;
25272
25430
  }
25273
- __name(getCloudflaredPath, "getCloudflaredPath");
25274
- async function spawnCloudflared(args, options) {
25275
- const logger = options?.logger;
25276
- const binPath = await getCloudflaredPath({
25277
- skipVersionCheck: options?.skipVersionCheck,
25278
- confirmDownload: options?.confirmDownload,
25279
- logger
25280
- });
25431
+ __name(queryUpdateService, "queryUpdateService");
25432
+ async function getLatestVersionInfo(options) {
25433
+ const { logger } = options ?? {};
25434
+ const goOS = getGoOS();
25435
+ const goArch = getGoArch();
25436
+ const primary = await queryUpdateService(goOS, goArch, { logger });
25437
+ if (primary && primary.url && primary.version) {
25438
+ return primary;
25439
+ }
25281
25440
  logger?.debug(
25282
- `Spawning cloudflared: ${binPath} ${redactCloudflaredArgsForLogging(args).join(" ")}`
25441
+ `Update worker had no result for ${goOS}/${goArch}, falling back to GitHub release URL`
25283
25442
  );
25284
- const cloudflared = spawn(binPath, args, {
25285
- stdio: options?.stdio ?? "inherit",
25286
- env: options?.env ? { ...process.env, ...options.env } : void 0
25287
- });
25288
- return cloudflared;
25289
- }
25290
- __name(spawnCloudflared, "spawnCloudflared");
25291
- function removeCloudflaredCache(version2) {
25292
- const cacheDir = version2 ? getCacheDir(version2) : join(getGlobalConfigPath(), "cloudflared");
25293
- if (existsSync(cacheDir)) {
25294
- removeDirSync(cacheDir);
25295
- return cacheDir;
25443
+ const fallback = await queryUpdateService("linux", "amd64", { logger });
25444
+ if (!fallback?.version) {
25445
+ throw new UserError(
25446
+ `[cloudflared] Failed to determine the latest cloudflared version.
25447
+
25448
+ The update service did not return results for ${goOS}/${goArch},
25449
+ and the fallback query also failed.
25450
+
25451
+ You can manually install cloudflared from:
25452
+ https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25453
+ { telemetryMessage: "tunnel cloudflared version lookup failed" }
25454
+ );
25296
25455
  }
25297
- return null;
25456
+ const version2 = fallback.version;
25457
+ const filename = getAssetFilename(goOS, goArch);
25458
+ const url2 = `${GITHUB_RELEASE_BASE}/${version2}/${filename}`;
25459
+ const compressed = filename.endsWith(".tgz");
25460
+ return {
25461
+ url: url2,
25462
+ version: version2,
25463
+ checksum: "",
25464
+ // no checksum available for fallback URLs
25465
+ compressed,
25466
+ shouldUpdate: true,
25467
+ userMessage: "",
25468
+ error: ""
25469
+ };
25298
25470
  }
25299
- __name(removeCloudflaredCache, "removeCloudflaredCache");
25300
-
25301
- // src/tunnel.ts
25302
- var TUNNEL_STARTUP_TIMEOUT_MS = 3e4;
25303
- var TUNNEL_FORCE_KILL_TIMEOUT_MS = 5e3;
25304
- var DEFAULT_TUNNEL_EXPIRY_MS = 60 * 60 * 1e3;
25305
- var DEFAULT_TUNNEL_EXTENSION_MS = 60 * 60 * 1e3;
25306
- var DEFAULT_TUNNEL_MAX_REMAINING_MS = 3 * 60 * 60 * 1e3;
25307
- var DEFAULT_TUNNEL_REMINDER_INTERVAL_MS = 10 * 60 * 1e3;
25308
- var QUICK_TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
25309
- function startTunnel(options) {
25310
- let disposed = false;
25311
- let reminderInterval;
25312
- let expiryTimeout;
25313
- let expiresAt = 0;
25314
- let cloudflaredProcess;
25315
- const logger = options.logger;
25316
- const timeoutMs = options.timeoutMs ?? TUNNEL_STARTUP_TIMEOUT_MS;
25317
- const reminderIntervalMs = options.reminderIntervalMs ?? DEFAULT_TUNNEL_REMINDER_INTERVAL_MS;
25318
- const defaultExpiryMs = options.expiryMs ?? DEFAULT_TUNNEL_EXPIRY_MS;
25319
- const isNamedTunnel = options.token !== void 0;
25320
- const timeFormatter = new Intl.DateTimeFormat(void 0, {
25321
- timeStyle: "short"
25322
- });
25323
- const cloudflaredArgs = isNamedTunnel ? ["tunnel", "--no-autoupdate", "run"] : ["tunnel", "--no-autoupdate", "--url", options.origin.href];
25324
- const cloudflaredPromise = spawnCloudflared(cloudflaredArgs, {
25325
- stdio: "pipe",
25326
- env: options.token ? { TUNNEL_TOKEN: options.token } : void 0,
25327
- skipVersionCheck: true,
25328
- logger
25329
- }).then((process4) => {
25330
- cloudflaredProcess = process4;
25331
- if (disposed) {
25332
- terminateCloudflared(process4);
25333
- }
25334
- return process4;
25335
- });
25336
- const readyPromise = cloudflaredPromise.then((process4) => {
25337
- if (isNamedTunnel) {
25338
- return { mode: "named" };
25471
+ __name(getLatestVersionInfo, "getLatestVersionInfo");
25472
+ function getCacheDir(version2) {
25473
+ return join(getGlobalConfigPath(), "cloudflared", version2);
25474
+ }
25475
+ __name(getCacheDir, "getCacheDir");
25476
+ function getCloudflaredBinPath(version2) {
25477
+ const binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
25478
+ return join(getCacheDir(version2), binName);
25479
+ }
25480
+ __name(getCloudflaredBinPath, "getCloudflaredBinPath");
25481
+ function isBinaryExecutable(binPath) {
25482
+ try {
25483
+ accessSync(binPath, constants.X_OK);
25484
+ return true;
25485
+ } catch {
25486
+ return false;
25487
+ }
25488
+ }
25489
+ __name(isBinaryExecutable, "isBinaryExecutable");
25490
+ function validateBinary(binPath, options) {
25491
+ const { logger } = options ?? {};
25492
+ try {
25493
+ const output = execFileSync(binPath, ["--version"], {
25494
+ stdio: ["pipe", "pipe", "pipe"],
25495
+ timeout: 1e4,
25496
+ encoding: "utf8"
25497
+ }).trim();
25498
+ logger?.debug(`cloudflared version: ${output}`);
25499
+ } catch {
25500
+ let errorMessage = `[cloudflared] Failed to validate cloudflared binary at ${binPath}
25501
+
25502
+ `;
25503
+ errorMessage += `This usually means:
25504
+ `;
25505
+ errorMessage += ` - The binary is corrupted or incomplete
25506
+ `;
25507
+ errorMessage += ` - You're missing required system libraries
25508
+ `;
25509
+ if (process.platform === "linux") {
25510
+ errorMessage += `
25511
+ On Linux, make sure you have the required dependencies:
25512
+ `;
25513
+ errorMessage += ` - glibc (GNU C Library)
25514
+ `;
25515
+ errorMessage += ` - For Debian/Ubuntu: sudo apt-get install libc6
25516
+ `;
25339
25517
  }
25340
- return waitForQuickTunnelReady(process4, timeoutMs, {
25341
- logger,
25342
- origin: options.origin
25518
+ const cacheDir = join(getGlobalConfigPath(), "cloudflared");
25519
+ errorMessage += `
25520
+ You can try:
25521
+ `;
25522
+ errorMessage += ` 1. Deleting the cache directory: rm -rf ${cacheDir}
25523
+ `;
25524
+ errorMessage += ` 2. Running the command again to re-download
25525
+ `;
25526
+ errorMessage += ` 3. Manually installing cloudflared: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/
25527
+ `;
25528
+ errorMessage += ` 4. Setting CLOUDFLARED_PATH to point to your cloudflared binary`;
25529
+ throw new UserError(errorMessage, {
25530
+ telemetryMessage: "tunnel cloudflared validation failed"
25343
25531
  });
25344
- }).then((result) => {
25345
- expiresAt = Date.now() + defaultExpiryMs;
25346
- scheduleExpiryTimeout();
25347
- scheduleReminder(
25348
- result.mode === "quick" ? result.publicUrl.origin : void 0
25349
- );
25350
- return result;
25351
- });
25352
- function disposeTunnel() {
25353
- disposed = true;
25354
- clearTunnelTimers();
25355
- if (cloudflaredProcess) {
25356
- terminateCloudflared(cloudflaredProcess);
25357
- }
25358
25532
  }
25359
- __name(disposeTunnel, "disposeTunnel");
25360
- function clearTunnelTimers() {
25361
- if (expiryTimeout) {
25362
- clearTimeout(expiryTimeout);
25363
- expiryTimeout = void 0;
25533
+ }
25534
+ __name(validateBinary, "validateBinary");
25535
+ function redactCloudflaredArgsForLogging(args) {
25536
+ const redacted = [...args];
25537
+ for (let i = 0; i < redacted.length; i++) {
25538
+ const arg = redacted[i];
25539
+ if (arg === "--token" && i + 1 < redacted.length) {
25540
+ redacted[i + 1] = "[REDACTED]";
25364
25541
  }
25365
- if (reminderInterval) {
25366
- clearInterval(reminderInterval);
25367
- reminderInterval = void 0;
25542
+ if (arg.startsWith("--token=")) {
25543
+ redacted[i] = "--token=[REDACTED]";
25368
25544
  }
25369
25545
  }
25370
- __name(clearTunnelTimers, "clearTunnelTimers");
25371
- function scheduleReminder(publicURL) {
25372
- if (reminderIntervalMs > 0) {
25373
- reminderInterval = setInterval(() => {
25374
- if (disposed) {
25375
- return;
25376
- }
25377
- const remainingMs = expiresAt - Date.now();
25378
- if (remainingMs <= 0) {
25379
- return;
25380
- }
25381
- logger?.log(
25382
- `${publicURL ? `Tunnel still open, expires in ${formatTunnelDuration(remainingMs)}: ${publicURL}` : `The tunnel is still open. It expires in ${formatTunnelDuration(remainingMs)}.`}${options.extendHint ? ` ${options.extendHint}` : ""}`
25383
- );
25384
- }, reminderIntervalMs);
25385
- reminderInterval.unref?.();
25386
- }
25546
+ return redacted;
25547
+ }
25548
+ __name(redactCloudflaredArgsForLogging, "redactCloudflaredArgsForLogging");
25549
+ function tryGetCloudflaredFromPath(options) {
25550
+ const { logger } = options ?? {};
25551
+ if (!(0, import_command_exists.sync)("cloudflared")) {
25552
+ return null;
25387
25553
  }
25388
- __name(scheduleReminder, "scheduleReminder");
25389
- function scheduleExpiryTimeout() {
25390
- if (disposed) {
25554
+ try {
25555
+ validateBinary("cloudflared", { logger });
25556
+ return "cloudflared";
25557
+ } catch (e2) {
25558
+ logger?.debug("cloudflared found in PATH but failed validation", e2);
25559
+ return null;
25560
+ }
25561
+ }
25562
+ __name(tryGetCloudflaredFromPath, "tryGetCloudflaredFromPath");
25563
+ function getInstalledVersion(binPath) {
25564
+ try {
25565
+ const output = execFileSync(binPath, ["--version"], {
25566
+ stdio: ["pipe", "pipe", "pipe"],
25567
+ timeout: 1e4,
25568
+ encoding: "utf8"
25569
+ });
25570
+ const match = output.match(/(\d+\.\d+\.\d+)/);
25571
+ return match ? match[1] : null;
25572
+ } catch {
25573
+ return null;
25574
+ }
25575
+ }
25576
+ __name(getInstalledVersion, "getInstalledVersion");
25577
+ function isVersionOutdated(installed, latest) {
25578
+ const parse3 = /* @__PURE__ */ __name((v) => v.split(".").map(Number), "parse");
25579
+ const [iYear, iMonth, iPatch] = parse3(installed);
25580
+ const [lYear, lMonth, lPatch] = parse3(latest);
25581
+ if (iYear !== lYear) {
25582
+ return iYear < lYear;
25583
+ }
25584
+ if (iMonth !== lMonth) {
25585
+ return iMonth < lMonth;
25586
+ }
25587
+ return iPatch < lPatch;
25588
+ }
25589
+ __name(isVersionOutdated, "isVersionOutdated");
25590
+ async function warnIfOutdated(binPath, options) {
25591
+ const { logger } = options ?? {};
25592
+ try {
25593
+ const installed = getInstalledVersion(binPath);
25594
+ if (!installed) {
25391
25595
  return;
25392
25596
  }
25393
- if (expiryTimeout) {
25394
- clearTimeout(expiryTimeout);
25395
- }
25396
- expiryTimeout = setTimeout(
25397
- () => {
25398
- if (disposed) {
25399
- return;
25400
- }
25401
- logger?.log("Tunnel expired. Closing tunnel.");
25402
- disposeTunnel();
25403
- },
25404
- Math.max(0, expiresAt - Date.now())
25405
- );
25406
- expiryTimeout.unref();
25407
- }
25408
- __name(scheduleExpiryTimeout, "scheduleExpiryTimeout");
25409
- function extendExpiry(ms = DEFAULT_TUNNEL_EXTENSION_MS) {
25410
- if (disposed || !expiryTimeout || ms <= 0) {
25597
+ const latest = await queryUpdateService(getGoOS(), getGoArch(), { logger }) ?? await queryUpdateService("linux", "amd64", { logger });
25598
+ const latestVersion = latest?.version;
25599
+ if (!latestVersion) {
25411
25600
  return;
25412
25601
  }
25413
- const now = Date.now();
25414
- const previousExpiresAt = expiresAt;
25415
- expiresAt = Math.min(
25416
- now + DEFAULT_TUNNEL_MAX_REMAINING_MS,
25417
- Math.max(expiresAt, now) + ms
25418
- );
25419
- const extendedByMs = expiresAt - previousExpiresAt;
25420
- if (extendedByMs < ms) {
25421
- logger?.log(
25422
- `Tunnel expiry extended to the ${formatTunnelDuration(DEFAULT_TUNNEL_MAX_REMAINING_MS)} limit. It now expires at ${timeFormatter.format(new Date(expiresAt))}.`
25602
+ if (isVersionOutdated(installed, latestVersion)) {
25603
+ logger?.warn(
25604
+ `Your cloudflared (${installed}) is outdated. Latest version is ${latestVersion}.
25605
+ Update: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`
25423
25606
  );
25424
- scheduleExpiryTimeout();
25425
- return;
25426
25607
  }
25427
- logger?.log(
25428
- `Tunnel expiry extended by ${formatTunnelDuration(extendedByMs)}. It now expires at ${timeFormatter.format(new Date(expiresAt))}.`
25429
- );
25430
- scheduleExpiryTimeout();
25608
+ } catch (e2) {
25609
+ logger?.debug("Failed to check cloudflared version", e2);
25431
25610
  }
25432
- __name(extendExpiry, "extendExpiry");
25433
- return {
25434
- ready: /* @__PURE__ */ __name(() => readyPromise, "ready"),
25435
- isOpen: /* @__PURE__ */ __name(() => !disposed, "isOpen"),
25436
- dispose: disposeTunnel,
25437
- extendExpiry
25438
- };
25439
25611
  }
25440
- __name(startTunnel, "startTunnel");
25441
- function formatTunnelDuration(durationMs) {
25442
- const totalMinutes = Math.max(1, Math.ceil(durationMs / 6e4));
25443
- const hours = Math.floor(totalMinutes / 60);
25444
- const minutes = totalMinutes % 60;
25445
- if (hours === 0) {
25446
- return `${minutes}m`;
25447
- }
25448
- if (minutes === 0) {
25449
- return `${hours}h`;
25612
+ __name(warnIfOutdated, "warnIfOutdated");
25613
+ function writeFileAtomic(filePath, contents) {
25614
+ const dir2 = dirname(filePath);
25615
+ const tmpPath = join(
25616
+ dir2,
25617
+ `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
25618
+ );
25619
+ try {
25620
+ writeFileSync(tmpPath, contents);
25621
+ renameSync(tmpPath, filePath);
25622
+ } finally {
25623
+ try {
25624
+ if (existsSync(tmpPath)) {
25625
+ unlinkSync(tmpPath);
25626
+ }
25627
+ } catch {
25628
+ }
25450
25629
  }
25451
- return `${hours}h ${minutes}m`;
25452
25630
  }
25453
- __name(formatTunnelDuration, "formatTunnelDuration");
25454
- function terminateCloudflared(cloudflared) {
25455
- if (cloudflared.killed) {
25456
- return;
25631
+ __name(writeFileAtomic, "writeFileAtomic");
25632
+ async function downloadCloudflared(versionInfo, binPath, options) {
25633
+ const { logger } = options ?? {};
25634
+ const { url: url2, version: version2, checksum, compressed } = versionInfo;
25635
+ logger?.log(`Downloading cloudflared ${version2}...`);
25636
+ logger?.debug(`Download URL: ${url2}`);
25637
+ const cacheDir = dirname(binPath);
25638
+ mkdirSync(cacheDir, { recursive: true });
25639
+ let response;
25640
+ try {
25641
+ response = await fetch(url2, {
25642
+ headers: { "User-Agent": "wrangler" }
25643
+ });
25644
+ } catch (e2) {
25645
+ throw new UserError(
25646
+ `[cloudflared] Failed to download cloudflared from ${url2}
25647
+
25648
+ Network error: ${e2 instanceof Error ? e2.message : String(e2)}
25649
+
25650
+ Please check your internet connection and try again.
25651
+ If you're behind a proxy, make sure it's configured correctly.`,
25652
+ { telemetryMessage: "tunnel cloudflared download network failed" }
25653
+ );
25457
25654
  }
25458
- cloudflared.unref();
25459
- cloudflared.kill("SIGTERM");
25460
- const forceKillTimer = setTimeout(() => {
25461
- if (!cloudflared.killed) {
25462
- cloudflared.kill("SIGKILL");
25655
+ if (!response.ok) {
25656
+ throw new UserError(
25657
+ `[cloudflared] Failed to download cloudflared from ${url2}
25658
+
25659
+ HTTP ${response.status}: ${response.statusText}
25660
+
25661
+ You can manually download cloudflared from:
25662
+ https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25663
+ { telemetryMessage: "tunnel cloudflared download response failed" }
25664
+ );
25665
+ }
25666
+ try {
25667
+ if (compressed) {
25668
+ await downloadAndExtractTarball(response, checksum, binPath, cacheDir);
25669
+ } else {
25670
+ await downloadBinary(response, checksum, binPath);
25463
25671
  }
25464
- }, TUNNEL_FORCE_KILL_TIMEOUT_MS);
25465
- forceKillTimer.unref();
25672
+ } catch (e2) {
25673
+ try {
25674
+ if (existsSync(binPath)) {
25675
+ unlinkSync(binPath);
25676
+ }
25677
+ } catch {
25678
+ }
25679
+ if (e2 instanceof UserError) {
25680
+ throw e2;
25681
+ }
25682
+ throw new UserError(
25683
+ `[cloudflared] Failed to save cloudflared binary
25684
+
25685
+ Error: ${e2 instanceof Error ? e2.message : String(e2)}
25686
+
25687
+ Please ensure you have write permissions to: ${cacheDir}`,
25688
+ { telemetryMessage: "tunnel cloudflared save failed" }
25689
+ );
25690
+ }
25691
+ if (process.platform !== "win32") {
25692
+ chmodSync(binPath, 493);
25693
+ }
25694
+ logger?.log(`cloudflared ${version2} installed`);
25695
+ logger?.debug(`Binary location: ${binPath}`);
25466
25696
  }
25467
- __name(terminateCloudflared, "terminateCloudflared");
25468
- function waitForQuickTunnelReady(cloudflared, timeoutMs, options) {
25469
- return new Promise((resolve3, reject) => {
25470
- let resolved = false;
25471
- let stderrOutput = "";
25472
- const logger = options?.logger;
25473
- const origin = options?.origin;
25474
- const timeoutId = setTimeout(() => {
25475
- if (!resolved) {
25476
- resolved = true;
25477
- terminateCloudflared(cloudflared);
25478
- reject(
25479
- createTunnelStartupError(
25480
- `Timed out waiting for cloudflared to start (${timeoutMs / 1e3}s).`,
25481
- stderrOutput,
25482
- origin
25483
- )
25697
+ __name(downloadCloudflared, "downloadCloudflared");
25698
+ async function downloadAndExtractTarball(response, expectedChecksum, binPath, cacheDir) {
25699
+ const tempTarPath = join(cacheDir, "cloudflared.tgz");
25700
+ const buffer = Buffer.from(await response.arrayBuffer());
25701
+ writeFileSync(tempTarPath, buffer);
25702
+ try {
25703
+ execFileSync("tar", ["-xzf", tempTarPath, "-C", cacheDir], {
25704
+ stdio: "ignore"
25705
+ });
25706
+ const extractedPath = join(cacheDir, "cloudflared");
25707
+ if (extractedPath !== binPath && existsSync(extractedPath)) {
25708
+ renameSync(extractedPath, binPath);
25709
+ }
25710
+ if (expectedChecksum) {
25711
+ const extractedBinary = readFileSync$1(binPath);
25712
+ const actualSha256 = sha256Hex(extractedBinary);
25713
+ if (actualSha256 !== expectedChecksum) {
25714
+ throw new UserError(
25715
+ `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.
25716
+
25717
+ Expected: ${expectedChecksum}
25718
+ Actual: ${actualSha256}`,
25719
+ {
25720
+ telemetryMessage: "tunnel cloudflared extracted binary checksum mismatch"
25721
+ }
25484
25722
  );
25485
25723
  }
25486
- }, timeoutMs);
25487
- timeoutId.unref();
25488
- if (cloudflared.stderr) {
25489
- cloudflared.stderr.on("data", (data) => {
25490
- const chunk = data.toString();
25491
- stderrOutput += chunk;
25492
- logger?.debug("[cloudflared]", chunk.trimEnd());
25493
- const match = QUICK_TUNNEL_URL_REGEX.exec(stderrOutput);
25494
- if (match && !resolved) {
25495
- resolved = true;
25496
- clearTimeout(timeoutId);
25497
- resolve3({ mode: "quick", publicUrl: new URL(match[0]) });
25498
- }
25499
- });
25500
25724
  }
25501
- cloudflared.on("error", (error51) => {
25502
- if (!resolved) {
25503
- resolved = true;
25504
- clearTimeout(timeoutId);
25505
- reject(new Error(`Failed to start cloudflared: ${error51.message}`));
25506
- }
25507
- });
25508
- cloudflared.on("exit", (code, signal) => {
25509
- if (!resolved) {
25510
- resolved = true;
25511
- clearTimeout(timeoutId);
25512
- const reason = signal ? `terminated by signal ${signal}` : `exited with code ${code}`;
25513
- reject(
25514
- createTunnelStartupError(
25515
- `cloudflared ${reason} before the tunnel was ready.`,
25516
- stderrOutput,
25517
- origin
25518
- )
25519
- );
25725
+ } finally {
25726
+ try {
25727
+ if (existsSync(tempTarPath)) {
25728
+ unlinkSync(tempTarPath);
25520
25729
  }
25521
- });
25522
- });
25523
- }
25524
- __name(waitForQuickTunnelReady, "waitForQuickTunnelReady");
25525
- function createTunnelStartupError(message, stderrOutput, origin) {
25526
- const isQuickTunnelRateLimited = stderrOutput.includes(
25527
- "429 Too Many Requests"
25528
- );
25529
- const errorMessage = `${message}
25530
- cloudflared output:
25531
- ${stderrOutput || "(no output)"}
25532
-
25533
- The local dev server started at ${origin.href}
25534
- ` + (isQuickTunnelRateLimited ? "Cloudflare Quick Tunnel creation was rate limited. Try again in a few minutes, or use a named tunnel if you need more reliable access." : `Check the cloudflared output above for more details, and verify that ${origin.href} is reachable from this machine if this keeps happening.`);
25535
- if (isQuickTunnelRateLimited) {
25536
- return new UserError(errorMessage, { telemetryMessage: false });
25730
+ } catch {
25731
+ }
25537
25732
  }
25538
- return new Error(errorMessage);
25539
25733
  }
25540
- __name(createTunnelStartupError, "createTunnelStartupError");
25734
+ __name(downloadAndExtractTarball, "downloadAndExtractTarball");
25735
+ async function downloadBinary(response, expectedChecksum, binPath) {
25736
+ const buffer = Buffer.from(await response.arrayBuffer());
25737
+ if (expectedChecksum) {
25738
+ const actualSha256 = sha256Hex(buffer);
25739
+ if (actualSha256 !== expectedChecksum) {
25740
+ throw new UserError(
25741
+ `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.
25541
25742
 
25542
- // src/cfetch/errors.ts
25543
- function buildDetailedError(message, ...extra) {
25544
- return new ParseError({
25545
- text: message,
25546
- notes: extra.map((text) => ({ text })),
25547
- telemetryMessage: false
25548
- });
25549
- }
25550
- __name(buildDetailedError, "buildDetailedError");
25551
- function maybeThrowFriendlyError(error51) {
25552
- if (error51.message === "workers.api.error.email_verification_required") {
25553
- throw buildDetailedError(
25554
- "Please verify your account's email address and try again.",
25555
- "Check your email for a verification link, or login to https://dash.cloudflare.com and request a new one."
25556
- );
25743
+ Expected: ${expectedChecksum}
25744
+ Actual: ${actualSha256}`,
25745
+ { telemetryMessage: "tunnel cloudflared binary checksum mismatch" }
25746
+ );
25747
+ }
25557
25748
  }
25749
+ writeFileAtomic(binPath, buffer);
25558
25750
  }
25559
- __name(maybeThrowFriendlyError, "maybeThrowFriendlyError");
25751
+ __name(downloadBinary, "downloadBinary");
25752
+ async function getCloudflaredPath(options) {
25753
+ const logger = options?.logger;
25754
+ const envPath = getCloudflaredPathFromEnv();
25755
+ if (envPath) {
25756
+ if (!existsSync(envPath)) {
25757
+ throw new UserError(
25758
+ `CLOUDFLARED_PATH is set to "${envPath}" but the file does not exist.
25560
25759
 
25561
- // src/cfetch/index.ts
25562
- function logHeaders(headers, logger) {
25563
- const clone2 = cloneHeaders(headers);
25564
- clone2.delete("Authorization");
25565
- logger.debugWithSanitization?.(
25566
- "HEADERS:",
25567
- JSON.stringify(Object.fromEntries(clone2), null, 2)
25568
- );
25569
- }
25570
- __name(logHeaders, "logHeaders");
25571
- async function performApiFetchBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25572
- assert2(credentials, "credentials are required for performApiFetch");
25573
- const method = init.method ?? "GET";
25574
- assert2(
25575
- resource.startsWith("/"),
25576
- `CF API fetch - resource path must start with a "/" but got "${resource}"`
25577
- );
25578
- const headers = cloneHeaders(new Headers(init.headers));
25579
- addAuthorizationHeader(headers, credentials);
25580
- headers.set("User-Agent", userAgent);
25581
- maybeAddTraceHeader(headers);
25582
- const queryString = queryParams ? `?${queryParams.toString()}` : "";
25583
- logger.debug(
25584
- `-- START CF API REQUEST: ${method} ${getCloudflareApiBaseUrl(complianceConfig)}${resource}`
25585
- );
25586
- logger.debugWithSanitization?.("QUERY STRING:", queryString);
25587
- logHeaders(headers, logger);
25588
- logger.debugWithSanitization?.("INIT:", JSON.stringify({ ...init }, null, 2));
25589
- if (init.body instanceof FormData) {
25590
- logger.debugWithSanitization?.(
25591
- "BODY:",
25592
- await new Response(init.body).text(),
25593
- null,
25594
- 2
25595
- );
25760
+ Please ensure the path points to a valid cloudflared binary.`,
25761
+ { telemetryMessage: "tunnel cloudflared env path missing" }
25762
+ );
25763
+ }
25764
+ logger?.debug(`Using cloudflared from CLOUDFLARED_PATH: ${envPath}`);
25765
+ return envPath;
25596
25766
  }
25597
- logger.debug("-- END CF API REQUEST");
25598
- return await fetch(
25599
- `${getCloudflareApiBaseUrl(complianceConfig)}${resource}${queryString}`,
25600
- {
25601
- method,
25602
- ...init,
25603
- headers,
25604
- signal: abortSignal
25767
+ const pathBin = tryGetCloudflaredFromPath({ logger });
25768
+ if (pathBin) {
25769
+ logger?.debug("Using cloudflared from PATH");
25770
+ if (!options?.skipVersionCheck) {
25771
+ await warnIfOutdated(pathBin, { logger });
25605
25772
  }
25606
- );
25607
- }
25608
- __name(performApiFetchBase, "performApiFetchBase");
25609
- async function fetchInternalBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25610
- const method = init.method ?? "GET";
25611
- const response = await performApiFetchBase(
25612
- complianceConfig,
25613
- resource,
25614
- init,
25615
- userAgent,
25616
- logger,
25617
- queryParams,
25618
- abortSignal,
25619
- credentials
25620
- );
25621
- const jsonText = await response.text();
25622
- logger.debug(
25623
- "-- START CF API RESPONSE:",
25624
- response.statusText,
25625
- response.status
25626
- );
25627
- logHeaders(response.headers, logger);
25628
- logger.debugWithSanitization?.("RESPONSE:", jsonText);
25629
- logger.debug("-- END CF API RESPONSE");
25630
- const retryAfterMs = parseRetryAfterMs(response.headers);
25631
- if (!jsonText && (response.status === 204 || response.status === 205)) {
25632
- return {
25633
- response: {
25634
- result: {},
25635
- success: true,
25636
- errors: [],
25637
- messages: []
25638
- },
25639
- status: response.status,
25640
- retryAfterMs
25641
- };
25773
+ return pathBin;
25642
25774
  }
25643
- if (isWAFBlockResponse(response.headers)) {
25644
- throwWAFBlockError(
25645
- response.headers,
25646
- method,
25647
- resource,
25648
- response.status,
25649
- response.statusText,
25650
- retryAfterMs
25775
+ const versionInfo = await getLatestVersionInfo({ logger });
25776
+ const binPath = getCloudflaredBinPath(versionInfo.version);
25777
+ let needsDownload = !isBinaryExecutable(binPath);
25778
+ if (!needsDownload) {
25779
+ try {
25780
+ validateBinary(binPath, { logger });
25781
+ logger?.debug(
25782
+ `Using cached cloudflared ${versionInfo.version}: ${binPath}`
25783
+ );
25784
+ return binPath;
25785
+ } catch (e2) {
25786
+ logger?.debug("Cached cloudflared failed validation; re-downloading", e2);
25787
+ needsDownload = true;
25788
+ }
25789
+ }
25790
+ const shouldDownload = await options?.confirmDownload?.(
25791
+ `cloudflared (${versionInfo.version}) is needed but not installed. Download to ${binPath}?`
25792
+ ) ?? true;
25793
+ if (!shouldDownload) {
25794
+ throw new UserError(
25795
+ `cloudflared is required to run this command.
25796
+
25797
+ You can install it manually from:
25798
+ https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/
25799
+
25800
+ Then either add it to your PATH or set CLOUDFLARED_PATH.`,
25801
+ { telemetryMessage: "tunnel cloudflared download declined" }
25651
25802
  );
25652
25803
  }
25653
- try {
25654
- const json2 = parseJSON(jsonText);
25655
- return { response: json2, status: response.status, retryAfterMs };
25656
- } catch {
25657
- const rayId = extractWAFBlockRayId(response.headers);
25658
- throw new APIError({
25659
- text: "Received a malformed response from the API",
25660
- notes: [
25661
- {
25662
- text: truncate(jsonText, 100)
25663
- },
25664
- {
25665
- text: `${method} ${resource} -> ${response.status} ${response.statusText}`
25666
- },
25667
- ...rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : []
25668
- ],
25669
- status: response.status,
25670
- retryAfterMs,
25671
- telemetryMessage: false
25672
- });
25804
+ if (existsSync(binPath)) {
25805
+ const cacheDir = removeCloudflaredCache(versionInfo.version);
25806
+ if (cacheDir) {
25807
+ logger?.log(`Removed cloudflared cache: ${cacheDir}`);
25808
+ }
25809
+ }
25810
+ await downloadCloudflared(versionInfo, binPath, { logger });
25811
+ validateBinary(binPath, { logger });
25812
+ return binPath;
25813
+ }
25814
+ __name(getCloudflaredPath, "getCloudflaredPath");
25815
+ async function spawnCloudflared(args, options) {
25816
+ const logger = options?.logger;
25817
+ const binPath = await getCloudflaredPath({
25818
+ skipVersionCheck: options?.skipVersionCheck,
25819
+ confirmDownload: options?.confirmDownload,
25820
+ logger
25821
+ });
25822
+ logger?.debug(
25823
+ `Spawning cloudflared: ${binPath} ${redactCloudflaredArgsForLogging(args).join(" ")}`
25824
+ );
25825
+ const cloudflared = spawn(binPath, args, {
25826
+ stdio: options?.stdio ?? "inherit",
25827
+ env: options?.env ? { ...process.env, ...options.env } : void 0
25828
+ });
25829
+ return cloudflared;
25830
+ }
25831
+ __name(spawnCloudflared, "spawnCloudflared");
25832
+ function removeCloudflaredCache(version2) {
25833
+ const cacheDir = version2 ? getCacheDir(version2) : join(getGlobalConfigPath(), "cloudflared");
25834
+ if (existsSync(cacheDir)) {
25835
+ removeDirSync(cacheDir);
25836
+ return cacheDir;
25673
25837
  }
25838
+ return null;
25674
25839
  }
25675
- __name(fetchInternalBase, "fetchInternalBase");
25676
- async function fetchResultBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25840
+ __name(removeCloudflaredCache, "removeCloudflaredCache");
25841
+
25842
+ // src/tunnel.ts
25843
+ var TUNNEL_STARTUP_TIMEOUT_MS = 3e4;
25844
+ var TUNNEL_API_TIMEOUT_MS = 6e4;
25845
+ var TUNNEL_FORCE_KILL_TIMEOUT_MS = 5e3;
25846
+ var DEFAULT_TUNNEL_EXPIRY_MS = 60 * 60 * 1e3;
25847
+ var DEFAULT_TUNNEL_EXTENSION_MS = 60 * 60 * 1e3;
25848
+ var DEFAULT_TUNNEL_MAX_REMAINING_MS = 3 * 60 * 60 * 1e3;
25849
+ var DEFAULT_TUNNEL_REMINDER_INTERVAL_MS = 10 * 60 * 1e3;
25850
+ var QUICK_TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
25851
+ var LOCAL_TUNNEL_HOSTNAMES = /* @__PURE__ */ new Set([
25852
+ "localhost",
25853
+ "127.0.0.1",
25854
+ "::1",
25855
+ "0.0.0.0",
25856
+ "::"
25857
+ ]);
25858
+ async function resolveNamedTunnel(name, origin, options) {
25677
25859
  const {
25678
- response: json2,
25679
- status,
25680
- retryAfterMs
25681
- } = await fetchInternalBase(
25860
+ accountId,
25861
+ apiToken,
25862
+ complianceRegion,
25863
+ logger,
25864
+ userAgent,
25865
+ abortSignal
25866
+ } = options;
25867
+ const complianceConfig = { compliance_region: complianceRegion };
25868
+ const resource = `/accounts/${accountId}/cfd_tunnel`;
25869
+ const tunnels = await fetchResultBase(
25682
25870
  complianceConfig,
25683
25871
  resource,
25684
- init,
25872
+ void 0,
25685
25873
  userAgent,
25686
25874
  logger,
25687
- queryParams,
25688
- abortSignal,
25689
- credentials
25875
+ new URLSearchParams({ name, is_deleted: "false" }),
25876
+ createApiAbortSignal(TUNNEL_API_TIMEOUT_MS, abortSignal),
25877
+ apiToken
25690
25878
  );
25691
- if (json2.success) {
25692
- return json2.result;
25693
- } else {
25694
- throwFetchError(resource, json2, status, retryAfterMs);
25879
+ const tunnel = tunnels.find((item) => item.name === name);
25880
+ if (!tunnel) {
25881
+ throw new UserError(
25882
+ `No Cloudflare Tunnel named "${name}" was found in this account. Use "wrangler tunnel list" to see available tunnels.`,
25883
+ { telemetryMessage: "tunnel resolve named missing tunnel" }
25884
+ );
25695
25885
  }
25696
- }
25697
- __name(fetchResultBase, "fetchResultBase");
25698
- async function fetchListResultBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, credentials) {
25699
- const results = [];
25700
- let getMoreResults = true;
25701
- let cursor;
25702
- while (getMoreResults) {
25703
- if (cursor) {
25704
- queryParams = new URLSearchParams(queryParams);
25705
- queryParams.set("cursor", cursor);
25706
- }
25707
- const {
25708
- response: json2,
25709
- status,
25710
- retryAfterMs
25711
- } = await fetchInternalBase(
25712
- complianceConfig,
25713
- resource,
25714
- init,
25715
- userAgent,
25716
- logger,
25717
- queryParams,
25718
- void 0,
25719
- credentials
25886
+ const tunnelId = tunnel.id;
25887
+ if (!tunnelId) {
25888
+ throw new FatalError(
25889
+ `Tunnel "${name}" was found but has no ID. This is unexpected.`,
25890
+ { telemetryMessage: "tunnel resolve named missing tunnel id" }
25720
25891
  );
25721
- if (json2.success) {
25722
- results.push(...json2.result);
25723
- if (hasCursor(json2.result_info)) {
25724
- cursor = json2.result_info?.cursor;
25725
- } else {
25726
- getMoreResults = false;
25727
- }
25728
- } else {
25729
- throwFetchError(resource, json2, status, retryAfterMs);
25730
- }
25731
25892
  }
25732
- return results;
25733
- }
25734
- __name(fetchListResultBase, "fetchListResultBase");
25735
- function truncate(text, maxLength) {
25736
- const { length } = text;
25737
- if (length <= maxLength) {
25738
- return text;
25893
+ const configuration = await fetchResultBase(
25894
+ complianceConfig,
25895
+ `${resource}/${tunnelId}/configurations`,
25896
+ void 0,
25897
+ userAgent,
25898
+ logger,
25899
+ void 0,
25900
+ createApiAbortSignal(TUNNEL_API_TIMEOUT_MS, abortSignal),
25901
+ apiToken
25902
+ );
25903
+ const ingress = configuration.config?.ingress ?? [];
25904
+ const hostnames = getMatchingIngressHostnames(origin, ingress);
25905
+ if (hostnames.length === 0) {
25906
+ throw new UserError(
25907
+ createMissingIngressMessage(name, origin, {
25908
+ dashboardUrl: `https://dash.cloudflare.com/${accountId}/tunnels/${tunnelId}`,
25909
+ ingress
25910
+ }),
25911
+ { telemetryMessage: "tunnel resolve named ingress mismatch" }
25912
+ );
25739
25913
  }
25740
- return `${text.substring(0, maxLength)}... (length = ${length})`;
25741
- }
25742
- __name(truncate, "truncate");
25743
- function isWAFBlockResponse(headers) {
25744
- return headers.get("cf-mitigated") === "challenge";
25914
+ const token = await fetchResultBase(
25915
+ complianceConfig,
25916
+ `${resource}/${tunnelId}/token`,
25917
+ void 0,
25918
+ userAgent,
25919
+ logger,
25920
+ void 0,
25921
+ createApiAbortSignal(TUNNEL_API_TIMEOUT_MS, abortSignal),
25922
+ apiToken
25923
+ );
25924
+ return { hostnames, token: String(token) };
25745
25925
  }
25746
- __name(isWAFBlockResponse, "isWAFBlockResponse");
25747
- function parseRetryAfterValue(retryAfter) {
25748
- if (!retryAfter) {
25749
- return void 0;
25926
+ __name(resolveNamedTunnel, "resolveNamedTunnel");
25927
+ function getMatchingIngressHostnames(origin, ingressConfig) {
25928
+ const hostnames = /* @__PURE__ */ new Set();
25929
+ const originUrl = normalizeURL(origin);
25930
+ for (const ingress of ingressConfig) {
25931
+ try {
25932
+ const serviceUrl = normalizeURL(ingress.service);
25933
+ if (ingress.hostname && serviceUrl.toString() === originUrl.toString()) {
25934
+ hostnames.add(ingress.hostname);
25935
+ }
25936
+ } catch {
25937
+ }
25750
25938
  }
25751
- if (/^\d+$/.test(retryAfter.trim())) {
25752
- return Number(retryAfter) * 1e3;
25939
+ return [...hostnames];
25940
+ }
25941
+ __name(getMatchingIngressHostnames, "getMatchingIngressHostnames");
25942
+ function normalizeURL(url2) {
25943
+ const normalizedUrl = new URL(url2);
25944
+ if (LOCAL_TUNNEL_HOSTNAMES.has(normalizedUrl.hostname)) {
25945
+ normalizedUrl.hostname = "localhost";
25753
25946
  }
25754
- const retryAfterDate = new Date(retryAfter);
25755
- if (!Number.isNaN(retryAfterDate.getTime())) {
25756
- return Math.max(0, retryAfterDate.getTime() - Date.now());
25947
+ if (!normalizedUrl.port) {
25948
+ switch (normalizedUrl.protocol) {
25949
+ case "http:":
25950
+ normalizedUrl.port = "80";
25951
+ break;
25952
+ case "https:":
25953
+ normalizedUrl.port = "443";
25954
+ break;
25955
+ }
25757
25956
  }
25758
- return void 0;
25759
- }
25760
- __name(parseRetryAfterValue, "parseRetryAfterValue");
25761
- function parseRetryAfterMs(headers) {
25762
- return parseRetryAfterValue(headers.get("Retry-After"));
25763
- }
25764
- __name(parseRetryAfterMs, "parseRetryAfterMs");
25765
- function extractWAFBlockRayId(headers) {
25766
- return headers.get("cf-ray") ?? void 0;
25767
- }
25768
- __name(extractWAFBlockRayId, "extractWAFBlockRayId");
25769
- function extractAccountTag(resource) {
25770
- const re = new RegExp("/accounts/([a-zA-Z0-9]+)/?");
25771
- const matches = re.exec(resource);
25772
- return matches?.[1];
25957
+ return normalizedUrl;
25773
25958
  }
25774
- __name(extractAccountTag, "extractAccountTag");
25775
- function hasMorePages(result_info) {
25776
- const page = result_info?.page;
25777
- const per_page = result_info?.per_page;
25778
- const total = result_info?.total_count;
25779
- return page !== void 0 && per_page !== void 0 && total !== void 0 && page * per_page < total;
25959
+ __name(normalizeURL, "normalizeURL");
25960
+ function createApiAbortSignal(timeoutMs, abortSignal) {
25961
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
25962
+ return abortSignal ? AbortSignal.any([abortSignal, timeoutSignal]) : timeoutSignal;
25780
25963
  }
25781
- __name(hasMorePages, "hasMorePages");
25782
- function renderError(err, level = 0) {
25783
- const indent = " ".repeat(level);
25784
- const message = err.message ?? "";
25785
- const chainedMessages = "error_chain" in err ? err.error_chain?.map(
25786
- (chainedError) => `
25787
-
25788
- ${indent}- ${renderError(chainedError, level + 1)}`
25789
- ).join("\n") ?? "" : "";
25790
- return (err.code ? `${message} [code: ${err.code}]` : message) + (err.documentation_url ? `
25791
- ${indent}To learn more about this error, visit: ${err.documentation_url}` : "") + chainedMessages;
25964
+ __name(createApiAbortSignal, "createApiAbortSignal");
25965
+ function createMissingIngressMessage(name, origin, {
25966
+ dashboardUrl,
25967
+ ingress
25968
+ }) {
25969
+ if (ingress.length === 0) {
25970
+ return [
25971
+ `Tunnel "${name}" has no routes configured.`,
25972
+ "",
25973
+ `Add a route for ${origin} in the Cloudflare dashboard:`,
25974
+ dashboardUrl,
25975
+ ""
25976
+ ].join("\n");
25977
+ }
25978
+ return [
25979
+ `Tunnel "${name}" has no route for ${origin}`,
25980
+ "",
25981
+ "Resolved routes:",
25982
+ ...ingress.map(
25983
+ ({ hostname: hostname3, service }) => ` - ${hostname3 ?? "(no hostname)"} -> ${service}`
25984
+ ),
25985
+ "",
25986
+ "Update your local server settings or the tunnel routes in the Cloudflare dashboard:",
25987
+ dashboardUrl,
25988
+ ""
25989
+ ].join("\n");
25792
25990
  }
25793
- __name(renderError, "renderError");
25794
- function addAuthorizationHeader(headers, auth, overrideExisting = false) {
25795
- if (!headers.has("Authorization") || overrideExisting) {
25796
- if ("apiToken" in auth) {
25797
- const authorizationHeader = `Bearer ${auth.apiToken}`;
25798
- validateAuthorizationHeaderValue(authorizationHeader);
25799
- headers.set("Authorization", authorizationHeader);
25800
- } else {
25801
- headers.set("X-Auth-Key", auth.authKey);
25802
- headers.set("X-Auth-Email", auth.authEmail);
25991
+ __name(createMissingIngressMessage, "createMissingIngressMessage");
25992
+ function startTunnel(options) {
25993
+ let disposed = false;
25994
+ let reminderInterval;
25995
+ let expiryTimeout;
25996
+ let expiresAt = 0;
25997
+ let cloudflaredProcess;
25998
+ const logger = options.logger;
25999
+ const timeoutMs = options.timeoutMs ?? TUNNEL_STARTUP_TIMEOUT_MS;
26000
+ const reminderIntervalMs = options.reminderIntervalMs ?? DEFAULT_TUNNEL_REMINDER_INTERVAL_MS;
26001
+ const defaultExpiryMs = options.expiryMs ?? DEFAULT_TUNNEL_EXPIRY_MS;
26002
+ const isNamedTunnel = options.token !== void 0;
26003
+ const timeFormatter = new Intl.DateTimeFormat(void 0, {
26004
+ timeStyle: "short"
26005
+ });
26006
+ const cloudflaredArgs = isNamedTunnel ? ["tunnel", "--no-autoupdate", "run"] : ["tunnel", "--no-autoupdate", "--url", options.origin.href];
26007
+ const cloudflaredPromise = spawnCloudflared(cloudflaredArgs, {
26008
+ stdio: "pipe",
26009
+ env: options.token ? { TUNNEL_TOKEN: options.token } : void 0,
26010
+ skipVersionCheck: true,
26011
+ logger
26012
+ }).then((process4) => {
26013
+ cloudflaredProcess = process4;
26014
+ if (disposed) {
26015
+ terminateCloudflared(process4);
26016
+ }
26017
+ return process4;
26018
+ });
26019
+ const readyPromise = cloudflaredPromise.then((process4) => {
26020
+ if (isNamedTunnel) {
26021
+ return { mode: "named" };
26022
+ }
26023
+ return waitForQuickTunnelReady(process4, timeoutMs, {
26024
+ logger,
26025
+ origin: options.origin
26026
+ });
26027
+ }).then((result) => {
26028
+ expiresAt = Date.now() + defaultExpiryMs;
26029
+ scheduleExpiryTimeout();
26030
+ scheduleReminder(
26031
+ result.mode === "quick" ? result.publicUrl.origin : void 0
26032
+ );
26033
+ return result;
26034
+ });
26035
+ function disposeTunnel() {
26036
+ disposed = true;
26037
+ clearTunnelTimers();
26038
+ if (cloudflaredProcess) {
26039
+ terminateCloudflared(cloudflaredProcess);
25803
26040
  }
25804
26041
  }
25805
- }
25806
- __name(addAuthorizationHeader, "addAuthorizationHeader");
25807
- function validateAuthorizationHeaderValue(value) {
25808
- for (const character of value) {
25809
- const codePoint = character.codePointAt(0);
25810
- if (codePoint === void 0 || codePoint > 255) {
25811
- throw new UserError(
25812
- `The configured Cloudflare API token contains a character that cannot be used in an HTTP Authorization header: ${formatAuthorizationHeaderCharacter(character, codePoint)}. Recreate or copy the token again, making sure it does not include characters such as ellipses.`,
25813
- {
25814
- telemetryMessage: "cfetch auth invalid authorization header"
26042
+ __name(disposeTunnel, "disposeTunnel");
26043
+ function clearTunnelTimers() {
26044
+ if (expiryTimeout) {
26045
+ clearTimeout(expiryTimeout);
26046
+ expiryTimeout = void 0;
26047
+ }
26048
+ if (reminderInterval) {
26049
+ clearInterval(reminderInterval);
26050
+ reminderInterval = void 0;
26051
+ }
26052
+ }
26053
+ __name(clearTunnelTimers, "clearTunnelTimers");
26054
+ function scheduleReminder(publicURL) {
26055
+ if (reminderIntervalMs > 0) {
26056
+ reminderInterval = setInterval(() => {
26057
+ if (disposed) {
26058
+ return;
26059
+ }
26060
+ const remainingMs = expiresAt - Date.now();
26061
+ if (remainingMs <= 0) {
26062
+ return;
26063
+ }
26064
+ logger?.log(
26065
+ `${publicURL ? `Tunnel still open, expires in ${formatTunnelDuration(remainingMs)}: ${publicURL}` : `The tunnel is still open. It expires in ${formatTunnelDuration(remainingMs)}.`}${options.extendHint ? ` ${options.extendHint}` : ""}`
26066
+ );
26067
+ }, reminderIntervalMs);
26068
+ reminderInterval.unref?.();
26069
+ }
26070
+ }
26071
+ __name(scheduleReminder, "scheduleReminder");
26072
+ function scheduleExpiryTimeout() {
26073
+ if (disposed) {
26074
+ return;
26075
+ }
26076
+ if (expiryTimeout) {
26077
+ clearTimeout(expiryTimeout);
26078
+ }
26079
+ expiryTimeout = setTimeout(
26080
+ () => {
26081
+ if (disposed) {
26082
+ return;
25815
26083
  }
26084
+ logger?.log("Tunnel expired. Closing tunnel.");
26085
+ disposeTunnel();
26086
+ },
26087
+ Math.max(0, expiresAt - Date.now())
26088
+ );
26089
+ expiryTimeout.unref();
26090
+ }
26091
+ __name(scheduleExpiryTimeout, "scheduleExpiryTimeout");
26092
+ function extendExpiry(ms = DEFAULT_TUNNEL_EXTENSION_MS) {
26093
+ if (disposed || !expiryTimeout || ms <= 0) {
26094
+ return;
26095
+ }
26096
+ const now = Date.now();
26097
+ const previousExpiresAt = expiresAt;
26098
+ expiresAt = Math.min(
26099
+ now + DEFAULT_TUNNEL_MAX_REMAINING_MS,
26100
+ Math.max(expiresAt, now) + ms
26101
+ );
26102
+ const extendedByMs = expiresAt - previousExpiresAt;
26103
+ if (extendedByMs < ms) {
26104
+ logger?.log(
26105
+ `Tunnel expiry extended to the ${formatTunnelDuration(DEFAULT_TUNNEL_MAX_REMAINING_MS)} limit. It now expires at ${timeFormatter.format(new Date(expiresAt))}.`
25816
26106
  );
26107
+ scheduleExpiryTimeout();
26108
+ return;
25817
26109
  }
26110
+ logger?.log(
26111
+ `Tunnel expiry extended by ${formatTunnelDuration(extendedByMs)}. It now expires at ${timeFormatter.format(new Date(expiresAt))}.`
26112
+ );
26113
+ scheduleExpiryTimeout();
25818
26114
  }
26115
+ __name(extendExpiry, "extendExpiry");
26116
+ return {
26117
+ ready: /* @__PURE__ */ __name(() => readyPromise, "ready"),
26118
+ isOpen: /* @__PURE__ */ __name(() => !disposed, "isOpen"),
26119
+ dispose: disposeTunnel,
26120
+ extendExpiry
26121
+ };
25819
26122
  }
25820
- __name(validateAuthorizationHeaderValue, "validateAuthorizationHeaderValue");
25821
- function formatAuthorizationHeaderCharacter(character, codePoint) {
25822
- if (codePoint === void 0) {
25823
- return '"\\u{unknown}"';
26123
+ __name(startTunnel, "startTunnel");
26124
+ function formatTunnelDuration(durationMs) {
26125
+ const totalMinutes = Math.max(1, Math.ceil(durationMs / 6e4));
26126
+ const hours = Math.floor(totalMinutes / 60);
26127
+ const minutes = totalMinutes % 60;
26128
+ if (hours === 0) {
26129
+ return `${minutes}m`;
25824
26130
  }
25825
- const codePointLabel = `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`;
25826
- const characterLabel = isPrintableCharacter(character) ? `"${character}"` : `"${escapeCharacter(character)}"`;
25827
- return `${characterLabel} (${codePointLabel})`;
25828
- }
25829
- __name(formatAuthorizationHeaderCharacter, "formatAuthorizationHeaderCharacter");
25830
- function isPrintableCharacter(character) {
25831
- return !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character);
26131
+ if (minutes === 0) {
26132
+ return `${hours}h`;
26133
+ }
26134
+ return `${hours}h ${minutes}m`;
25832
26135
  }
25833
- __name(isPrintableCharacter, "isPrintableCharacter");
25834
- function escapeCharacter(character) {
25835
- return Array.from(character).map((c) => {
25836
- const codePoint = c.codePointAt(0);
25837
- if (codePoint === void 0) {
25838
- return "";
26136
+ __name(formatTunnelDuration, "formatTunnelDuration");
26137
+ function terminateCloudflared(cloudflared) {
26138
+ if (cloudflared.killed) {
26139
+ return;
26140
+ }
26141
+ cloudflared.unref();
26142
+ cloudflared.kill("SIGTERM");
26143
+ const forceKillTimer = setTimeout(() => {
26144
+ if (!cloudflared.killed) {
26145
+ cloudflared.kill("SIGKILL");
25839
26146
  }
25840
- return codePoint <= 65535 ? `\\u${codePoint.toString(16).toUpperCase().padStart(4, "0")}` : `\\u{${codePoint.toString(16).toUpperCase()}}`;
25841
- }).join("");
26147
+ }, TUNNEL_FORCE_KILL_TIMEOUT_MS);
26148
+ forceKillTimer.unref();
25842
26149
  }
25843
- __name(escapeCharacter, "escapeCharacter");
25844
- function throwFetchError(resource, response, status, retryAfterMs) {
25845
- const errors = response.errors ?? [];
25846
- for (const error52 of errors) {
25847
- maybeThrowFriendlyError(error52);
25848
- }
25849
- const notes = [
25850
- ...errors.map((err) => ({ text: renderError(err) })),
25851
- ...response.messages?.map((msg) => ({
25852
- text: typeof msg === "string" ? msg : msg.message ?? String(msg)
25853
- })) ?? []
25854
- ];
25855
- if (notes.length === 0) {
25856
- const raw = response;
25857
- const fallbackMessage = typeof raw.error === "string" ? `${raw.error}${raw.code ? ` [code: ${raw.code}]` : ""}` : void 0;
25858
- if (fallbackMessage) {
25859
- notes.push({ text: fallbackMessage });
26150
+ __name(terminateCloudflared, "terminateCloudflared");
26151
+ function waitForQuickTunnelReady(cloudflared, timeoutMs, options) {
26152
+ return new Promise((resolve3, reject) => {
26153
+ let resolved = false;
26154
+ let stderrOutput = "";
26155
+ const logger = options?.logger;
26156
+ const origin = options?.origin;
26157
+ const timeoutId = setTimeout(() => {
26158
+ if (!resolved) {
26159
+ resolved = true;
26160
+ terminateCloudflared(cloudflared);
26161
+ reject(
26162
+ createTunnelStartupError(
26163
+ `Timed out waiting for cloudflared to start (${timeoutMs / 1e3}s).`,
26164
+ stderrOutput,
26165
+ origin
26166
+ )
26167
+ );
26168
+ }
26169
+ }, timeoutMs);
26170
+ timeoutId.unref();
26171
+ if (cloudflared.stderr) {
26172
+ cloudflared.stderr.on("data", (data) => {
26173
+ const chunk = data.toString();
26174
+ stderrOutput += chunk;
26175
+ logger?.debug("[cloudflared]", chunk.trimEnd());
26176
+ const match = QUICK_TUNNEL_URL_REGEX.exec(stderrOutput);
26177
+ if (match && !resolved) {
26178
+ resolved = true;
26179
+ clearTimeout(timeoutId);
26180
+ resolve3({ mode: "quick", publicUrl: new URL(match[0]) });
26181
+ }
26182
+ });
25860
26183
  }
25861
- }
25862
- if (retryAfterMs !== void 0) {
25863
- notes.push({
25864
- text: `The API responded with a "Retry-After" header indicating you should wait ${Math.ceil(retryAfterMs / 1e3)} second(s) before retrying.`
26184
+ cloudflared.on("error", (error51) => {
26185
+ if (!resolved) {
26186
+ resolved = true;
26187
+ clearTimeout(timeoutId);
26188
+ reject(new Error(`Failed to start cloudflared: ${error51.message}`));
26189
+ }
25865
26190
  });
25866
- }
25867
- const error51 = new APIError({
25868
- text: `A request to the Cloudflare API (${resource}) failed.`,
25869
- notes,
25870
- status,
25871
- // hoist the parsed `Retry-After` header (if any) so consumers such as
25872
- // `retryOnAPIFailure()` can back off for the amount of time the API
25873
- // asked us to wait, e.g. when rate limited (HTTP 429).
25874
- retryAfterMs,
25875
- telemetryMessage: false
25876
- });
25877
- const code = errors[0]?.code;
25878
- if (code) {
25879
- error51.code = code;
25880
- }
25881
- const meta3 = errors[0]?.meta;
25882
- if (meta3) {
25883
- error51.meta = meta3;
25884
- }
25885
- error51.accountTag = extractAccountTag(resource);
25886
- throw error51;
25887
- }
25888
- __name(throwFetchError, "throwFetchError");
25889
- function throwWAFBlockError(headers, method, resource, status, statusText, retryAfterMs) {
25890
- const rayId = extractWAFBlockRayId(headers);
25891
- throw new APIError({
25892
- text: "The Cloudflare API responded with a WAF block page instead of the expected JSON response",
25893
- notes: [
25894
- {
25895
- text: "Cloudflare's firewall (WAF) blocked this API request. This is usually a false positive."
25896
- },
25897
- ...rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : [],
25898
- {
25899
- text: rayId ? "If the issue persists, please open a Cloudflare Support ticket and include the Ray ID above." : "If the issue persists, please open a Cloudflare Support ticket. You can find the Cloudflare Ray ID on the block page in your browser."
25900
- },
25901
- {
25902
- text: `${method} ${resource} -> ${status} ${statusText}`
26191
+ cloudflared.on("exit", (code, signal) => {
26192
+ if (!resolved) {
26193
+ resolved = true;
26194
+ clearTimeout(timeoutId);
26195
+ const reason = signal ? `terminated by signal ${signal}` : `exited with code ${code}`;
26196
+ reject(
26197
+ createTunnelStartupError(
26198
+ `cloudflared ${reason} before the tunnel was ready.`,
26199
+ stderrOutput,
26200
+ origin
26201
+ )
26202
+ );
25903
26203
  }
25904
- ],
25905
- status,
25906
- retryAfterMs,
25907
- telemetryMessage: false
25908
- });
25909
- }
25910
- __name(throwWAFBlockError, "throwWAFBlockError");
25911
- async function fetchKVGetValueBase(complianceConfig, accountId, namespaceId, key, userAgent, logger, credentials) {
25912
- const headers = new Headers();
25913
- addAuthorizationHeader(headers, credentials);
25914
- headers.set("User-Agent", userAgent);
25915
- maybeAddTraceHeader(headers);
25916
- const resource = `${getCloudflareApiBaseUrl(complianceConfig)}/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`;
25917
- logger.debug(`-- START CF API REQUEST: GET ${resource}`);
25918
- logger.debug("-- END CF API REQUEST");
25919
- const response = await fetch(resource, {
25920
- method: "GET",
25921
- headers
26204
+ });
25922
26205
  });
25923
- if (response.ok) {
25924
- return await response.arrayBuffer();
25925
- } else {
25926
- throw new Error(
25927
- `Failed to fetch ${resource} - ${response.status}: ${response.statusText}`
25928
- );
25929
- }
25930
- }
25931
- __name(fetchKVGetValueBase, "fetchKVGetValueBase");
25932
- function hasCursor(result_info) {
25933
- const cursor = result_info?.cursor;
25934
- return cursor !== void 0 && cursor !== null && cursor !== "";
25935
26206
  }
25936
- __name(hasCursor, "hasCursor");
25937
- function maybeAddTraceHeader(headers) {
25938
- const traceHeader = getTraceHeader();
25939
- if (traceHeader) {
25940
- headers.set("Cf-Trace-Id", traceHeader);
26207
+ __name(waitForQuickTunnelReady, "waitForQuickTunnelReady");
26208
+ function createTunnelStartupError(message, stderrOutput, origin) {
26209
+ const isQuickTunnelRateLimited = stderrOutput.includes(
26210
+ "429 Too Many Requests"
26211
+ );
26212
+ const errorMessage = `${message}
26213
+ cloudflared output:
26214
+ ${stderrOutput || "(no output)"}
26215
+
26216
+ The local dev server started at ${origin.href}
26217
+ ` + (isQuickTunnelRateLimited ? "Cloudflare Quick Tunnel creation was rate limited. Try again in a few minutes, or use a named tunnel if you need more reliable access." : `Check the cloudflared output above for more details, and verify that ${origin.href} is reachable from this machine if this keeps happening.`);
26218
+ if (isQuickTunnelRateLimited) {
26219
+ return new UserError(errorMessage, { telemetryMessage: false });
25941
26220
  }
26221
+ return new Error(errorMessage);
25942
26222
  }
25943
- __name(maybeAddTraceHeader, "maybeAddTraceHeader");
25944
- function cloneHeaders(headers) {
25945
- return new Headers(headers);
25946
- }
25947
- __name(cloneHeaders, "cloneHeaders");
26223
+ __name(createTunnelStartupError, "createTunnelStartupError");
25948
26224
 
25949
26225
  // src/update-check.ts
25950
26226
  var import_update_check = __toESM(require_update_check());
@@ -26277,4 +26553,4 @@ safe-buffer/index.js:
26277
26553
  (*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> *)
26278
26554
  */
26279
26555
 
26280
- export { ARTIFACTS_EVENT_TYPES, BunPackageManager, Diagnostics, LOGGER_LEVELS, NpmPackageManager, NubPackageManager, PatchConfigError, PnpmPackageManager, YarnPackageManager, addAuthorizationHeader, bucketFormatMessage, checkWorkerNameValidity, clearOutputFilePath, convertConfigToBindings, createConfigCache, defaultWranglerConfig, experimental_patchConfig, extractAccountTag, extractBindingsOfType, extractWAFBlockRayId, fetchInternalBase, fetchKVGetValueBase, fetchLatestNpmVersion, fetchListResultBase, fetchResultBase, formatTime, friendlyBindingNames2 as friendlyBindingNames, getBindingLocalSupport, getBindingTypeFriendlyName, getBindings, getHostFromRoute, getHostFromUrl, getInstalledPackageVersion, getPackagePath, getWorkerName, getWorkerNameFromProject, getWranglerHiddenDirPath, getWranglerTmpDir, getZoneFromRoute, hasCursor, hasMorePages, hasProperty, isCI, isDockerfile, isInteractive, isNonInteractiveOrCI, isOptionalProperty, isPackageInstalled, isPagesConfig, isRequiredProperty, isUnsafeBindingType, isValidR2BucketName, isWAFBlockResponse, maybeAddTraceHeader, normalizeAndValidateConfig, openInBrowser, parseRetryAfterMs, parseRetryAfterValue, performApiFetchBase, printBindings, renderError, retryOnAPIFailure, spawnCloudflared, startTunnel, sweepStaleWranglerTmpDirs, throwFetchError, toUrlPath, toValidWorkerName, truncate, validateBindingRemoteSetting, validatePagesConfig, writeOutput };
26556
+ export { ARTIFACTS_EVENT_TYPES, BunPackageManager, Diagnostics, LOGGER_LEVELS, NpmPackageManager, NubPackageManager, PatchConfigError, PnpmPackageManager, YarnPackageManager, addAuthorizationHeader, bucketFormatMessage, checkWorkerNameValidity, clearOutputFilePath, convertConfigToBindings, createConfigCache, defaultWranglerConfig, experimental_patchConfig, extractAccountTag, extractBindingsOfType, extractWAFBlockRayId, fetchInternalBase, fetchKVGetValueBase, fetchLatestNpmVersion, fetchListResultBase, fetchResultBase, formatTime, friendlyBindingNames2 as friendlyBindingNames, getBindingLocalSupport, getBindingTypeFriendlyName, getBindings, getHostFromRoute, getHostFromUrl, getInstalledPackageVersion, getPackagePath, getWorkerName, getWorkerNameFromProject, getWranglerHiddenDirPath, getWranglerTmpDir, getZoneFromRoute, hasCursor, hasMorePages, hasProperty, isCI, isDockerfile, isInteractive, isNonInteractiveOrCI, isOptionalProperty, isPackageInstalled, isPagesConfig, isRequiredProperty, isUnsafeBindingType, isValidR2BucketName, isWAFBlockResponse, maybeAddTraceHeader, normalizeAndValidateConfig, openInBrowser, parseRetryAfterMs, parseRetryAfterValue, performApiFetchBase, printBindings, renderError, resolveNamedTunnel, retryOnAPIFailure, spawnCloudflared, startTunnel, sweepStaleWranglerTmpDirs, throwFetchError, toUrlPath, toValidWorkerName, truncate, validateBindingRemoteSetting, validatePagesConfig, writeOutput };