@cloudflare/workers-utils 0.36.0 → 0.38.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
@@ -1,12 +1,12 @@
1
1
  export { MetricsRegistry } from './chunk-Z62CVNHJ.mjs';
2
2
  export { _forceColour, formatZodError } from './chunk-7AX24RFK.mjs';
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
- 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';
3
+ import { isRedirectedRawConfig, configFileName, formatConfigSnippet, getDurableObjectExports, getContainerNameToClassNameMap, parsePackageJSON, readFileSync, ParseError, parseJSON, APIError, CONTAINER_IMAGES_BINDING, dir, parseTOML, modify, applyEdits, format, dist_default, parseJSONC } from './chunk-IQDQ5WRD.mjs';
4
+ export { APIError, CONTAINER_IMAGES_BINDING, ENVIRONMENT_TAG_PREFIX, INHERIT_SYMBOL, JSON_CONFIG_FORMATS, PATH_TO_DEPLOY_CONFIG, ParseError, SERVICE_TAG_PREFIX, configFileName, configFormat, experimental_readRawConfig, findWranglerConfig, formatConfigSnippet, getContainerDurableObjectClassNames, getContainerNameToClassNameMap, getDurableObjectContainerApps, getDurableObjectExports, hasDurableObjectExports, indexLocation, isDurableObjectContainerApp, isRedirectedConfig, parseByteSize, parseHumanDuration, parseJSON, parseJSONC, parseNonHyphenedUuid, parsePackageJSON, parseTOML, partitionExports, readFileSync, readFileSyncToBuffer, resolveContainerClassName, resolveWranglerConfigPath, searchLocation } from './chunk-IQDQ5WRD.mjs';
5
+ import { assertNever } from './chunk-S3ANQ67D.mjs';
6
+ export { DEFAULT_COMPAT_DATE, assertNever, constructWranglerConfig, mapWorkerMetadataBindings } from './chunk-S3ANQ67D.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';
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';
8
+ import { getCloudflareEnv, getCloudflareContainerRegistry, getCloudflareApiBaseUrl, getTraceHeader, getCloudflaredPathFromEnv, getOutputFilePathFromEnv, getOutputFileDirectoryFromEnv, getCIOverrideName, getWranglerCacheDirFromEnv } from './chunk-IYSONYTF.mjs';
9
+ export { COMPLIANCE_REGION_CONFIG_PUBLIC, COMPLIANCE_REGION_CONFIG_UNKNOWN, getBrowserRenderingHeadfulFromEnv, getBuildConditionsFromEnv, getBuildPlatformFromEnv, getC3CommandFromEnv, getCIGeneratePreviewAlias, getCIMatchTag, getCIOverrideName, getCIOverrideNetworkModeHost, getCfFetchEnabledFromEnv, getCfFetchPathFromEnv, getCloudflareApiBaseUrl, getCloudflareApiEnvironmentFromEnv, getCloudflareComplianceRegion, getCloudflareContainerRegistry, 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-IYSONYTF.mjs';
10
10
  import { dedent } from './chunk-MCNIGVAF.mjs';
11
11
  export { getBooleanEnvironmentVariableFactory, getDockerPath, getEnvironmentVariableFactory } from './chunk-3IYLA2JH.mjs';
12
12
  import { UserError, FatalError } from './chunk-WO2IAW6M.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';
@@ -18451,7 +18451,16 @@ function normalizeAndValidateEnvironment(diagnostics, configPath, rawEnv, isDisp
18451
18451
  // `name` is inheritable, so a named environment that doesn't redeclare it
18452
18452
  // still runs under the top level Worker name — fall back to it so the
18453
18453
  // generated container name isn't built from `undefined`.
18454
- validateContainerApp(envName, rawEnv.name ?? rawConfig?.name, configPath),
18454
+ validateContainerApp(
18455
+ envName,
18456
+ rawEnv.name ?? rawConfig?.name,
18457
+ configPath,
18458
+ {
18459
+ complianceConfig: {
18460
+ compliance_region: rawEnv.compliance_region ?? topLevelEnv?.compliance_region
18461
+ }
18462
+ }
18463
+ ),
18455
18464
  void 0
18456
18465
  ),
18457
18466
  send_email: notInheritable(
@@ -19903,6 +19912,15 @@ function validatePreviewsContainers(envName, configPath) {
19903
19912
  });
19904
19913
  return (diagnostics, field, value, config2) => {
19905
19914
  if (Array.isArray(value)) {
19915
+ const durableObjectPolicyFields = [...value.entries()].filter(
19916
+ ([, entry]) => entry && typeof entry === "object" && entry.scheduling_policy === "durable_object"
19917
+ ).map(([index]) => `"${field}[${index}].scheduling_policy"`);
19918
+ if (durableObjectPolicyFields.length > 0) {
19919
+ diagnostics.errors.push(
19920
+ `${durableObjectPolicyFields.join(", ")} cannot be "durable_object". Durable Object-managed Containers are configured only in the top-level "containers" array.`
19921
+ );
19922
+ return false;
19923
+ }
19906
19924
  const nameFields = [...value.entries()].filter(
19907
19925
  ([, entry]) => entry && typeof entry === "object" && "name" in entry
19908
19926
  ).map(([index]) => `"${field}[${index}].name"`);
@@ -19937,6 +19955,85 @@ function validatePreviewsContainers(envName, configPath) {
19937
19955
  };
19938
19956
  }
19939
19957
  __name(validatePreviewsContainers, "validatePreviewsContainers");
19958
+ function validateDurableObjectContainerImages(diagnostics, field, images, complianceConfig) {
19959
+ if (images === void 0) {
19960
+ return true;
19961
+ }
19962
+ if (typeof images !== "object" || images === null || Array.isArray(images) || Object.keys(images).length === 0) {
19963
+ diagnostics.errors.push(
19964
+ `"${field}" must be a non-empty object when present.`
19965
+ );
19966
+ return false;
19967
+ }
19968
+ let valid = true;
19969
+ const entries = Object.entries(images);
19970
+ if (entries.length > 100) {
19971
+ diagnostics.errors.push(`"${field}" must contain at most 100 images.`);
19972
+ valid = false;
19973
+ }
19974
+ for (const [imageName, imageValue] of entries) {
19975
+ const imageField = `${field}.${imageName}`;
19976
+ if (imageName.length === 0 || imageName.length > 128) {
19977
+ diagnostics.errors.push(
19978
+ `"${field}" image names must be between 1 and 128 characters.`
19979
+ );
19980
+ valid = false;
19981
+ }
19982
+ if (typeof imageValue !== "object" || imageValue === null || Array.isArray(imageValue)) {
19983
+ diagnostics.errors.push(
19984
+ `"${imageField}" must be an object with either a "dockerfile" or "image" field.`
19985
+ );
19986
+ valid = false;
19987
+ continue;
19988
+ }
19989
+ const image = imageValue;
19990
+ const hasDockerfile = image.dockerfile !== void 0;
19991
+ const hasImage = image.image !== void 0;
19992
+ if (hasDockerfile === hasImage) {
19993
+ diagnostics.errors.push(
19994
+ `"${imageField}" must specify exactly one of "dockerfile" or "image".`
19995
+ );
19996
+ valid = false;
19997
+ }
19998
+ if (hasDockerfile && (typeof image.dockerfile !== "string" || image.dockerfile.length === 0)) {
19999
+ diagnostics.errors.push(
20000
+ `"${imageField}.dockerfile" must be a non-empty string.`
20001
+ );
20002
+ valid = false;
20003
+ }
20004
+ if (hasImage && (typeof image.image !== "string" || image.image.length === 0)) {
20005
+ diagnostics.errors.push(
20006
+ `"${imageField}.image" must be a non-empty string.`
20007
+ );
20008
+ valid = false;
20009
+ }
20010
+ if (typeof image.image === "string" && image.image.length > 0) {
20011
+ const registry2 = getCloudflareContainerRegistry(complianceConfig);
20012
+ const prefix = `${registry2}/`;
20013
+ const reference = image.image.slice(prefix.length);
20014
+ const digestReference = reference.match(
20015
+ /^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)+@sha256:[a-f0-9]{64}$/
20016
+ );
20017
+ if (!image.image.startsWith(prefix) || digestReference?.[0] !== reference) {
20018
+ diagnostics.errors.push(
20019
+ `"${imageField}.image" must be a digest-pinned image in the managed registry, in the form "${registry2}/<account-id>/<repository>@sha256:<64 lowercase hex characters>".`
20020
+ );
20021
+ valid = false;
20022
+ }
20023
+ }
20024
+ const unsupportedFields = Object.keys(image).filter(
20025
+ (property) => property !== "dockerfile" && property !== "image"
20026
+ );
20027
+ if (unsupportedFields.length > 0) {
20028
+ diagnostics.errors.push(
20029
+ `Unexpected fields found in ${imageField} field: ${unsupportedFields.map((property) => `"${property}"`).join(", ")}`
20030
+ );
20031
+ valid = false;
20032
+ }
20033
+ }
20034
+ return valid;
20035
+ }
20036
+ __name(validateDurableObjectContainerImages, "validateDurableObjectContainerImages");
19940
20037
  function validateContainerApp(envName, topLevelName, configPath, options = {}) {
19941
20038
  const { generateDefaultName = true } = options;
19942
20039
  return (diagnostics, field, value, config2) => {
@@ -19950,13 +20047,23 @@ function validateContainerApp(envName, topLevelName, configPath, options = {}) {
19950
20047
  return false;
19951
20048
  }
19952
20049
  for (const containerAppOptional of value) {
19953
- validateOptionalProperty(
19954
- diagnostics,
19955
- field,
19956
- "class_name",
19957
- containerAppOptional.class_name,
19958
- "string"
19959
- );
20050
+ const isDurableObjectManaged = containerAppOptional.scheduling_policy === "durable_object";
20051
+ const hasValidDurableObjectClassName = typeof containerAppOptional.class_name === "string" && containerAppOptional.class_name.length > 0;
20052
+ if (isDurableObjectManaged) {
20053
+ if (!hasValidDurableObjectClassName) {
20054
+ diagnostics.errors.push(
20055
+ `"containers.class_name" must be a non-empty string when "containers.scheduling_policy" is "durable_object".`
20056
+ );
20057
+ }
20058
+ } else {
20059
+ validateOptionalProperty(
20060
+ diagnostics,
20061
+ field,
20062
+ "class_name",
20063
+ containerAppOptional.class_name,
20064
+ "string"
20065
+ );
20066
+ }
19960
20067
  validateOptionalProperty(
19961
20068
  diagnostics,
19962
20069
  field,
@@ -19964,7 +20071,7 @@ function validateContainerApp(envName, topLevelName, configPath, options = {}) {
19964
20071
  containerAppOptional.name,
19965
20072
  "string"
19966
20073
  );
19967
- if (generateDefaultName && !containerAppOptional.name) {
20074
+ if (generateDefaultName && !containerAppOptional.name && (!isDurableObjectManaged || hasValidDurableObjectClassName)) {
19968
20075
  if (containerAppOptional.class_name === void 0) {
19969
20076
  diagnostics.errors.push(
19970
20077
  `"containers.name" is required when "containers.class_name" is not defined, because there is no class name to derive a default name from. Either name this container and reference it from a Durable Object's \`exports\` entry, or set "containers.class_name".`
@@ -19979,6 +20086,23 @@ function validateContainerApp(envName, topLevelName, configPath, options = {}) {
19979
20086
  containerAppOptional.name = name.toLowerCase().replace(/ /g, "-");
19980
20087
  }
19981
20088
  }
20089
+ if (isDurableObjectManaged) {
20090
+ validateDurableObjectContainerImages(
20091
+ diagnostics,
20092
+ `${field}.images`,
20093
+ containerAppOptional.images,
20094
+ options.complianceConfig
20095
+ );
20096
+ const unsupportedFields = Object.keys(containerAppOptional).filter(
20097
+ (key) => !["name", "class_name", "scheduling_policy", "images"].includes(key)
20098
+ );
20099
+ if (unsupportedFields.length > 0) {
20100
+ diagnostics.errors.push(
20101
+ `Unsupported fields for Durable Object-managed Containers in ${field}: ${unsupportedFields.map((key) => `"${key}"`).join(",")}. Only "name", "class_name", "scheduling_policy", and "images" are supported.`
20102
+ );
20103
+ }
20104
+ continue;
20105
+ }
19982
20106
  if (!containerAppOptional.configuration?.image && !containerAppOptional.image) {
19983
20107
  diagnostics.errors.push(
19984
20108
  `"containers.image" field must be defined for each container app. This should be the path to your Dockerfile or an image URI pointing to the Cloudflare registry.`
@@ -20117,6 +20241,12 @@ function validateContainerApp(envName, topLevelName, configPath, options = {}) {
20117
20241
  containerAppOptional.image_vars,
20118
20242
  "object"
20119
20243
  );
20244
+ validateContainerObservability(
20245
+ diagnostics,
20246
+ `${field}.observability`,
20247
+ containerAppOptional.observability,
20248
+ config2
20249
+ );
20120
20250
  validateOptionalProperty(
20121
20251
  diagnostics,
20122
20252
  field,
@@ -20160,6 +20290,7 @@ function validateContainerApp(envName, topLevelName, configPath, options = {}) {
20160
20290
  "image",
20161
20291
  "image_build_context",
20162
20292
  "image_vars",
20293
+ "observability",
20163
20294
  "class_name",
20164
20295
  "scheduling_policy",
20165
20296
  "instance_type",
@@ -20985,6 +21116,7 @@ var validateBindingsHaveUniqueNames = /* @__PURE__ */ __name((diagnostics, confi
20985
21116
  ])
20986
21117
  );
20987
21118
  bindingsGroupedByType["Secret"] = config2.secrets?.required ?? [];
21119
+ bindingsGroupedByType["Container images"] = [CONTAINER_IMAGES_BINDING];
20988
21120
  const bindingsGroupedByName = {};
20989
21121
  for (const bindingType in bindingsGroupedByType) {
20990
21122
  const bindingNames = bindingsGroupedByType[bindingType];
@@ -22400,6 +22532,133 @@ var validateExports = /* @__PURE__ */ __name((diagnostics, field, value) => {
22400
22532
  }
22401
22533
  return valid;
22402
22534
  }, "validateExports");
22535
+ var CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MIN = 1;
22536
+ var CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MAX = 99;
22537
+ var CONTAINER_OBSERVABILITY_TARGET_INSTANCE_COUNT_MIN = 1;
22538
+ function isContainerObservabilityEnabled(observability) {
22539
+ return observability?.logs?.enabled === true || observability?.enabled === true;
22540
+ }
22541
+ __name(isContainerObservabilityEnabled, "isContainerObservabilityEnabled");
22542
+ function hasConflictingContainerObservabilityEnabledValues(observability) {
22543
+ return typeof observability.enabled === "boolean" && typeof observability.logs?.enabled === "boolean" && observability.enabled !== observability.logs.enabled;
22544
+ }
22545
+ __name(hasConflictingContainerObservabilityEnabledValues, "hasConflictingContainerObservabilityEnabledValues");
22546
+ var validateContainerObservability = /* @__PURE__ */ __name((diagnostics, field, value) => {
22547
+ if (value === void 0) {
22548
+ return true;
22549
+ }
22550
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
22551
+ diagnostics.errors.push(
22552
+ `"${field}" should be an object but got ${JSON.stringify(value)}.`
22553
+ );
22554
+ return false;
22555
+ }
22556
+ const val = value;
22557
+ let isValid = true;
22558
+ isValid = validateOptionalProperty(
22559
+ diagnostics,
22560
+ field,
22561
+ "enabled",
22562
+ val.enabled,
22563
+ "boolean"
22564
+ ) && isValid;
22565
+ if (val.logs !== void 0) {
22566
+ if (typeof val.logs !== "object" || val.logs === null || Array.isArray(val.logs)) {
22567
+ diagnostics.errors.push(
22568
+ `Expected "${field}.logs" to be of type object but got ${JSON.stringify(
22569
+ val.logs
22570
+ )}.`
22571
+ );
22572
+ isValid = false;
22573
+ } else {
22574
+ isValid = validateOptionalProperty(
22575
+ diagnostics,
22576
+ field,
22577
+ "logs.enabled",
22578
+ val.logs.enabled,
22579
+ "boolean"
22580
+ ) && isValid;
22581
+ isValid = validateAdditionalProperties(
22582
+ diagnostics,
22583
+ `${field}.logs`,
22584
+ Object.keys(val.logs),
22585
+ ["enabled"]
22586
+ ) && isValid;
22587
+ }
22588
+ }
22589
+ 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)) {
22590
+ isValid = validateAtLeastOnePropertyRequired(diagnostics, field, [
22591
+ {
22592
+ key: "enabled",
22593
+ value: val.enabled,
22594
+ type: "boolean"
22595
+ },
22596
+ {
22597
+ key: "logs.enabled",
22598
+ value: val.logs?.enabled,
22599
+ type: "boolean"
22600
+ }
22601
+ ]) && isValid;
22602
+ }
22603
+ isValid = validateOptionalProperty(
22604
+ diagnostics,
22605
+ field,
22606
+ "target_instance_percentage",
22607
+ val.target_instance_percentage,
22608
+ "number"
22609
+ ) && isValid;
22610
+ isValid = validateOptionalProperty(
22611
+ diagnostics,
22612
+ field,
22613
+ "target_instance_count",
22614
+ val.target_instance_count,
22615
+ "number"
22616
+ ) && isValid;
22617
+ isValid = validateAdditionalProperties(diagnostics, field, Object.keys(val), [
22618
+ "enabled",
22619
+ "logs",
22620
+ "target_instance_percentage",
22621
+ "target_instance_count"
22622
+ ]) && isValid;
22623
+ if (hasConflictingContainerObservabilityEnabledValues(val)) {
22624
+ diagnostics.errors.push(
22625
+ `"${field}.enabled" and "${field}.logs.enabled" cannot be set to different values.`
22626
+ );
22627
+ isValid = false;
22628
+ }
22629
+ if (val.target_instance_percentage !== void 0 && val.target_instance_count !== void 0) {
22630
+ diagnostics.errors.push(
22631
+ `"${field}.target_instance_percentage" and "${field}.target_instance_count" cannot both be set.`
22632
+ );
22633
+ isValid = false;
22634
+ }
22635
+ 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)) {
22636
+ diagnostics.errors.push(
22637
+ `"${field}.target_instance_percentage" must be an integer between ${CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MIN} and ${CONTAINER_OBSERVABILITY_TARGET_INSTANCE_PERCENTAGE_MAX} inclusive.`
22638
+ );
22639
+ isValid = false;
22640
+ }
22641
+ if (typeof val.target_instance_count === "number" && (!Number.isInteger(val.target_instance_count) || val.target_instance_count < CONTAINER_OBSERVABILITY_TARGET_INSTANCE_COUNT_MIN)) {
22642
+ diagnostics.errors.push(
22643
+ `"${field}.target_instance_count" must be a positive integer.`
22644
+ );
22645
+ isValid = false;
22646
+ }
22647
+ const observabilityEnabled = isContainerObservabilityEnabled(val);
22648
+ if (val.target_instance_percentage !== void 0 && !observabilityEnabled) {
22649
+ diagnostics.errors.push(
22650
+ `"${field}.target_instance_percentage" requires "${field}.enabled" or "${field}.logs.enabled" to be true because container observability overrides root observability.`
22651
+ );
22652
+ isValid = false;
22653
+ }
22654
+ if (val.target_instance_count !== void 0 && !observabilityEnabled) {
22655
+ diagnostics.errors.push(
22656
+ `"${field}.target_instance_count" requires "${field}.enabled" or "${field}.logs.enabled" to be true because container observability overrides root observability.`
22657
+ );
22658
+ isValid = false;
22659
+ }
22660
+ return isValid;
22661
+ }, "validateContainerObservability");
22403
22662
  var validateObservability = /* @__PURE__ */ __name((diagnostics, field, value) => {
22404
22663
  if (value === void 0) {
22405
22664
  return true;
@@ -24305,7 +24564,9 @@ ${allTailConsumers.map(({ service, streaming }) => {
24305
24564
  }
24306
24565
  log(
24307
24566
  `${containersTitle}
24308
- ${containers.map((c) => `- ${c.name} (${c.image})`).join("\n")}`
24567
+ ${containers.map(
24568
+ (container) => container.scheduling_policy === "durable_object" ? `- ${container.class_name} (durable_object)` : `- ${container.name} (${container.image})`
24569
+ ).join("\n")}`
24309
24570
  );
24310
24571
  log("");
24311
24572
  }
@@ -24783,1168 +25044,1310 @@ function getWranglerTmpDir(projectRoot, prefix, cleanup = true) {
24783
25044
  }
24784
25045
  __name(getWranglerTmpDir, "getWranglerTmpDir");
24785
25046
 
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");
25047
+ // src/cfetch/errors.ts
25048
+ function buildDetailedError(message, ...extra) {
25049
+ return new ParseError({
25050
+ text: message,
25051
+ notes: extra.map((text) => ({ text })),
25052
+ telemetryMessage: false
25053
+ });
24792
25054
  }
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
- );
25055
+ __name(buildDetailedError, "buildDetailedError");
25056
+ function maybeThrowFriendlyError(error51) {
25057
+ if (error51.message === "workers.api.error.email_verification_required") {
25058
+ throw buildDetailedError(
25059
+ "Please verify your account's email address and try again.",
25060
+ "Check your email for a verification link, or login to https://dash.cloudflare.com and request a new one."
25061
+ );
24813
25062
  }
24814
25063
  }
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)
25064
+ __name(maybeThrowFriendlyError, "maybeThrowFriendlyError");
24829
25065
 
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
- }
25066
+ // src/cfetch/index.ts
25067
+ function logHeaders(headers, logger) {
25068
+ const clone2 = cloneHeaders(headers);
25069
+ clone2.delete("Authorization");
25070
+ logger.debugWithSanitization?.(
25071
+ "HEADERS:",
25072
+ JSON.stringify(Object.fromEntries(clone2), null, 2)
25073
+ );
24835
25074
  }
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`;
25075
+ __name(logHeaders, "logHeaders");
25076
+ async function performApiFetchBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25077
+ assert2(credentials, "credentials are required for performApiFetch");
25078
+ const method = init.method ?? "GET";
25079
+ assert2(
25080
+ resource.startsWith("/"),
25081
+ `CF API fetch - resource path must start with a "/" but got "${resource}"`
25082
+ );
25083
+ const headers = cloneHeaders(new Headers(init.headers));
25084
+ addAuthorizationHeader(headers, credentials);
25085
+ headers.set("User-Agent", userAgent);
25086
+ maybeAddTraceHeader(headers);
25087
+ const queryString = queryParams ? `?${queryParams.toString()}` : "";
25088
+ logger.debug(
25089
+ `-- START CF API REQUEST: ${method} ${getCloudflareApiBaseUrl(complianceConfig)}${resource}`
25090
+ );
25091
+ logger.debugWithSanitization?.("QUERY STRING:", queryString);
25092
+ logHeaders(headers, logger);
25093
+ logger.debugWithSanitization?.("INIT:", JSON.stringify({ ...init }, null, 2));
25094
+ if (init.body instanceof FormData) {
25095
+ logger.debugWithSanitization?.(
25096
+ "BODY:",
25097
+ await new Response(init.body).text(),
25098
+ null,
25099
+ 2
25100
+ );
24841
25101
  }
24842
- if (goOS === "darwin") {
24843
- return `cloudflared-${goOS}-${goArch}.tgz`;
24844
- }
24845
- return `cloudflared-${goOS}-${goArch}`;
25102
+ logger.debug("-- END CF API REQUEST");
25103
+ return await fetch(
25104
+ `${getCloudflareApiBaseUrl(complianceConfig)}${resource}${queryString}`,
25105
+ {
25106
+ method,
25107
+ ...init,
25108
+ headers,
25109
+ signal: abortSignal
25110
+ }
25111
+ );
24846
25112
  }
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;
25113
+ __name(performApiFetchBase, "performApiFetchBase");
25114
+ async function fetchInternalBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25115
+ const method = init.method ?? "GET";
25116
+ const response = await performApiFetchBase(
25117
+ complianceConfig,
25118
+ resource,
25119
+ init,
25120
+ userAgent,
25121
+ logger,
25122
+ queryParams,
25123
+ abortSignal,
25124
+ credentials
25125
+ );
25126
+ const jsonText = await response.text();
25127
+ logger.debug(
25128
+ "-- START CF API RESPONSE:",
25129
+ response.statusText,
25130
+ response.status
25131
+ );
25132
+ logHeaders(response.headers, logger);
25133
+ logger.debugWithSanitization?.("RESPONSE:", jsonText);
25134
+ logger.debug("-- END CF API RESPONSE");
25135
+ const retryAfterMs = parseRetryAfterMs(response.headers);
25136
+ if (!jsonText && (response.status === 204 || response.status === 205)) {
25137
+ return {
25138
+ response: {
25139
+ result: {},
25140
+ success: true,
25141
+ errors: [],
25142
+ messages: []
25143
+ },
25144
+ status: response.status,
25145
+ retryAfterMs
25146
+ };
24864
25147
  }
24865
- if (!response.ok) {
24866
- logger?.debug(
24867
- `Update service returned ${response.status} for ${goOS}/${goArch}`
25148
+ if (isWAFBlockResponse(response.headers)) {
25149
+ throwWAFBlockError(
25150
+ response.headers,
25151
+ method,
25152
+ resource,
25153
+ response.status,
25154
+ response.statusText,
25155
+ retryAfterMs
24868
25156
  );
24869
- return null;
24870
25157
  }
24871
- let data;
24872
25158
  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;
25159
+ const json2 = parseJSON(jsonText);
25160
+ return { response: json2, status: response.status, retryAfterMs };
25161
+ } catch {
25162
+ const rayId = extractWAFBlockRayId(response.headers);
25163
+ throw new APIError({
25164
+ text: "Received a malformed response from the API",
25165
+ notes: [
25166
+ {
25167
+ text: truncate(jsonText, 100)
25168
+ },
25169
+ {
25170
+ text: `${method} ${resource} -> ${response.status} ${response.statusText}`
25171
+ },
25172
+ ...rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : []
25173
+ ],
25174
+ status: response.status,
25175
+ retryAfterMs,
25176
+ telemetryMessage: false
25177
+ });
24879
25178
  }
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}`
25179
+ }
25180
+ __name(fetchInternalBase, "fetchInternalBase");
25181
+ async function fetchResultBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25182
+ const {
25183
+ response: json2,
25184
+ status,
25185
+ retryAfterMs
25186
+ } = await fetchInternalBase(
25187
+ complianceConfig,
25188
+ resource,
25189
+ init,
25190
+ userAgent,
25191
+ logger,
25192
+ queryParams,
25193
+ abortSignal,
25194
+ credentials
25195
+ );
25196
+ if (json2.success) {
25197
+ return json2.result;
25198
+ } else {
25199
+ throwFetchError(resource, json2, status, retryAfterMs);
25200
+ }
25201
+ }
25202
+ __name(fetchResultBase, "fetchResultBase");
25203
+ async function fetchListResultBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, credentials) {
25204
+ const results = [];
25205
+ let getMoreResults = true;
25206
+ let cursor;
25207
+ while (getMoreResults) {
25208
+ if (cursor) {
25209
+ queryParams = new URLSearchParams$1(queryParams);
25210
+ queryParams.set("cursor", cursor);
25211
+ }
25212
+ const {
25213
+ response: json2,
25214
+ status,
25215
+ retryAfterMs
25216
+ } = await fetchInternalBase(
25217
+ complianceConfig,
25218
+ resource,
25219
+ init,
25220
+ userAgent,
25221
+ logger,
25222
+ queryParams,
25223
+ void 0,
25224
+ credentials
24883
25225
  );
25226
+ if (json2.success) {
25227
+ results.push(...json2.result);
25228
+ if (hasCursor(json2.result_info)) {
25229
+ cursor = json2.result_info?.cursor;
25230
+ } else {
25231
+ getMoreResults = false;
25232
+ }
25233
+ } else {
25234
+ throwFetchError(resource, json2, status, retryAfterMs);
25235
+ }
24884
25236
  }
24885
- if (data.error || !data.url || !data.version) {
24886
- return data.version ? data : null;
25237
+ return results;
25238
+ }
25239
+ __name(fetchListResultBase, "fetchListResultBase");
25240
+ function truncate(text, maxLength) {
25241
+ const { length } = text;
25242
+ if (length <= maxLength) {
25243
+ return text;
24887
25244
  }
24888
- return data;
25245
+ return `${text.substring(0, maxLength)}... (length = ${length})`;
24889
25246
  }
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;
25247
+ __name(truncate, "truncate");
25248
+ function isWAFBlockResponse(headers) {
25249
+ return headers.get("cf-mitigated") === "challenge";
25250
+ }
25251
+ __name(isWAFBlockResponse, "isWAFBlockResponse");
25252
+ function parseRetryAfterValue(retryAfter) {
25253
+ if (!retryAfter) {
25254
+ return void 0;
24898
25255
  }
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
- );
25256
+ if (/^\d+$/.test(retryAfter.trim())) {
25257
+ return Number(retryAfter) * 1e3;
24914
25258
  }
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
- };
25259
+ const retryAfterDate = new Date(retryAfter);
25260
+ if (!Number.isNaN(retryAfterDate.getTime())) {
25261
+ return Math.max(0, retryAfterDate.getTime() - Date.now());
25262
+ }
25263
+ return void 0;
24929
25264
  }
24930
- __name(getLatestVersionInfo, "getLatestVersionInfo");
24931
- function getCacheDir(version2) {
24932
- return join(getGlobalConfigPath(), "cloudflared", version2);
25265
+ __name(parseRetryAfterValue, "parseRetryAfterValue");
25266
+ function parseRetryAfterMs(headers) {
25267
+ return parseRetryAfterValue(headers.get("Retry-After"));
24933
25268
  }
24934
- __name(getCacheDir, "getCacheDir");
24935
- function getCloudflaredBinPath(version2) {
24936
- const binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
24937
- return join(getCacheDir(version2), binName);
25269
+ __name(parseRetryAfterMs, "parseRetryAfterMs");
25270
+ function extractWAFBlockRayId(headers) {
25271
+ return headers.get("cf-ray") ?? void 0;
24938
25272
  }
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
- }
25273
+ __name(extractWAFBlockRayId, "extractWAFBlockRayId");
25274
+ function extractAccountTag(resource) {
25275
+ const re = new RegExp("/accounts/([a-zA-Z0-9]+)/?");
25276
+ const matches = re.exec(resource);
25277
+ return matches?.[1];
24947
25278
  }
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}
25279
+ __name(extractAccountTag, "extractAccountTag");
25280
+ function hasMorePages(result_info) {
25281
+ const page = result_info?.page;
25282
+ const per_page = result_info?.per_page;
25283
+ const total = result_info?.total_count;
25284
+ return page !== void 0 && per_page !== void 0 && total !== void 0 && page * per_page < total;
25285
+ }
25286
+ __name(hasMorePages, "hasMorePages");
25287
+ function renderError(err, level = 0) {
25288
+ const indent = " ".repeat(level);
25289
+ const message = err.message ?? "";
25290
+ const chainedMessages = "error_chain" in err ? err.error_chain?.map(
25291
+ (chainedError) => `
24960
25292
 
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
- }
25293
+ ${indent}- ${renderError(chainedError, level + 1)}`
25294
+ ).join("\n") ?? "" : "";
25295
+ return (err.code ? `${message} [code: ${err.code}]` : message) + (err.documentation_url ? `
25296
+ ${indent}To learn more about this error, visit: ${err.documentation_url}` : "") + chainedMessages;
24992
25297
  }
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]";
25298
+ __name(renderError, "renderError");
25299
+ function addAuthorizationHeader(headers, auth, overrideExisting = false) {
25300
+ if (!headers.has("Authorization") || overrideExisting) {
25301
+ if ("apiToken" in auth) {
25302
+ const authorizationHeader = `Bearer ${auth.apiToken}`;
25303
+ validateAuthorizationHeaderValue(authorizationHeader);
25304
+ headers.set("Authorization", authorizationHeader);
25305
+ } else {
25306
+ headers.set("X-Auth-Key", auth.authKey);
25307
+ headers.set("X-Auth-Email", auth.authEmail);
25003
25308
  }
25004
25309
  }
25005
- return redacted;
25006
25310
  }
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;
25311
+ __name(addAuthorizationHeader, "addAuthorizationHeader");
25312
+ function validateAuthorizationHeaderValue(value) {
25313
+ for (const character of value) {
25314
+ const codePoint = character.codePointAt(0);
25315
+ if (codePoint === void 0 || codePoint > 255) {
25316
+ throw new UserError(
25317
+ `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.`,
25318
+ {
25319
+ telemetryMessage: "cfetch auth invalid authorization header"
25320
+ }
25321
+ );
25322
+ }
25019
25323
  }
25020
25324
  }
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;
25325
+ __name(validateAuthorizationHeaderValue, "validateAuthorizationHeaderValue");
25326
+ function formatAuthorizationHeaderCharacter(character, codePoint) {
25327
+ if (codePoint === void 0) {
25328
+ return '"\\u{unknown}"';
25033
25329
  }
25330
+ const codePointLabel = `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`;
25331
+ const characterLabel = isPrintableCharacter(character) ? `"${character}"` : `"${escapeCharacter(character)}"`;
25332
+ return `${characterLabel} (${codePointLabel})`;
25034
25333
  }
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;
25334
+ __name(formatAuthorizationHeaderCharacter, "formatAuthorizationHeaderCharacter");
25335
+ function isPrintableCharacter(character) {
25336
+ return !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character);
25047
25337
  }
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
- );
25338
+ __name(isPrintableCharacter, "isPrintableCharacter");
25339
+ function escapeCharacter(character) {
25340
+ return Array.from(character).map((c) => {
25341
+ const codePoint = c.codePointAt(0);
25342
+ if (codePoint === void 0) {
25343
+ return "";
25066
25344
  }
25067
- } catch (e2) {
25068
- logger?.debug("Failed to check cloudflared version", e2);
25069
- }
25345
+ return codePoint <= 65535 ? `\\u${codePoint.toString(16).toUpperCase().padStart(4, "0")}` : `\\u{${codePoint.toString(16).toUpperCase()}}`;
25346
+ }).join("");
25070
25347
  }
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 {
25348
+ __name(escapeCharacter, "escapeCharacter");
25349
+ function throwFetchError(resource, response, status, retryAfterMs) {
25350
+ const errors = response.errors ?? [];
25351
+ for (const error52 of errors) {
25352
+ maybeThrowFriendlyError(error52);
25353
+ }
25354
+ const notes = [
25355
+ ...errors.map((err) => ({ text: renderError(err) })),
25356
+ ...response.messages?.map((msg) => ({
25357
+ text: typeof msg === "string" ? msg : msg.message ?? String(msg)
25358
+ })) ?? []
25359
+ ];
25360
+ if (notes.length === 0) {
25361
+ const raw = response;
25362
+ const fallbackMessage = typeof raw.error === "string" ? `${raw.error}${raw.code ? ` [code: ${raw.code}]` : ""}` : void 0;
25363
+ if (fallbackMessage) {
25364
+ notes.push({ text: fallbackMessage });
25087
25365
  }
25088
25366
  }
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" }
25367
+ if (retryAfterMs !== void 0) {
25368
+ notes.push({
25369
+ text: `The API responded with a "Retry-After" header indicating you should wait ${Math.ceil(retryAfterMs / 1e3)} second(s) before retrying.`
25102
25370
  });
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
25371
  }
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
- );
25372
+ const error51 = new APIError({
25373
+ text: `A request to the Cloudflare API (${resource}) failed.`,
25374
+ notes,
25375
+ status,
25376
+ // hoist the parsed `Retry-After` header (if any) so consumers such as
25377
+ // `retryOnAPIFailure()` can back off for the amount of time the API
25378
+ // asked us to wait, e.g. when rate limited (HTTP 429).
25379
+ retryAfterMs,
25380
+ telemetryMessage: false
25381
+ });
25382
+ const code = errors[0]?.code;
25383
+ if (code) {
25384
+ error51.code = code;
25124
25385
  }
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);
25135
- }
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" }
25386
+ const meta3 = errors[0]?.meta;
25387
+ if (meta3) {
25388
+ error51.meta = meta3;
25389
+ }
25390
+ error51.accountTag = extractAccountTag(resource);
25391
+ throw error51;
25392
+ }
25393
+ __name(throwFetchError, "throwFetchError");
25394
+ function throwWAFBlockError(headers, method, resource, status, statusText, retryAfterMs) {
25395
+ const rayId = extractWAFBlockRayId(headers);
25396
+ throw new APIError({
25397
+ text: "The Cloudflare API responded with a WAF block page instead of the expected JSON response",
25398
+ notes: [
25399
+ {
25400
+ text: "Cloudflare's firewall (WAF) blocked this API request. This is usually a false positive."
25401
+ },
25402
+ ...rayId ? [{ text: `Cloudflare Ray ID: ${rayId}` }] : [],
25403
+ {
25404
+ 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."
25405
+ },
25406
+ {
25407
+ text: `${method} ${resource} -> ${status} ${statusText}`
25408
+ }
25409
+ ],
25410
+ status,
25411
+ retryAfterMs,
25412
+ telemetryMessage: false
25413
+ });
25414
+ }
25415
+ __name(throwWAFBlockError, "throwWAFBlockError");
25416
+ async function fetchKVGetValueBase(complianceConfig, accountId, namespaceId, key, userAgent, logger, credentials) {
25417
+ const headers = new Headers();
25418
+ addAuthorizationHeader(headers, credentials);
25419
+ headers.set("User-Agent", userAgent);
25420
+ maybeAddTraceHeader(headers);
25421
+ const resource = `${getCloudflareApiBaseUrl(complianceConfig)}/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`;
25422
+ logger.debug(`-- START CF API REQUEST: GET ${resource}`);
25423
+ logger.debug("-- END CF API REQUEST");
25424
+ const response = await fetch(resource, {
25425
+ method: "GET",
25426
+ headers
25427
+ });
25428
+ if (response.ok) {
25429
+ return await response.arrayBuffer();
25430
+ } else {
25431
+ throw new Error(
25432
+ `Failed to fetch ${resource} - ${response.status}: ${response.statusText}`
25148
25433
  );
25149
25434
  }
25150
- if (process.platform !== "win32") {
25151
- chmodSync(binPath, 493);
25435
+ }
25436
+ __name(fetchKVGetValueBase, "fetchKVGetValueBase");
25437
+ function hasCursor(result_info) {
25438
+ const cursor = result_info?.cursor;
25439
+ return cursor !== void 0 && cursor !== null && cursor !== "";
25440
+ }
25441
+ __name(hasCursor, "hasCursor");
25442
+ function maybeAddTraceHeader(headers) {
25443
+ const traceHeader = getTraceHeader();
25444
+ if (traceHeader) {
25445
+ headers.set("Cf-Trace-Id", traceHeader);
25152
25446
  }
25153
- logger?.log(`cloudflared ${version2} installed`);
25154
- logger?.debug(`Binary location: ${binPath}`);
25155
25447
  }
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.
25448
+ __name(maybeAddTraceHeader, "maybeAddTraceHeader");
25449
+ function cloneHeaders(headers) {
25450
+ return new Headers(headers);
25451
+ }
25452
+ __name(cloneHeaders, "cloneHeaders");
25175
25453
 
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
- }
25454
+ // src/cloudflared.ts
25455
+ var import_command_exists = __toESM(require_command_exists2());
25456
+ var UPDATE_SERVICE_URL = "https://update.argotunnel.com";
25457
+ var CLOUDFLARED_VERSION_PATTERN = /^\d{4}\.\d+\.\d+$/;
25458
+ function sha256Hex(buffer) {
25459
+ return createHash("sha256").update(buffer).digest("hex");
25192
25460
  }
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) {
25461
+ __name(sha256Hex, "sha256Hex");
25462
+ function getGoArch() {
25463
+ const nodeArch = arch();
25464
+ switch (nodeArch) {
25465
+ case "x64":
25466
+ return "amd64";
25467
+ case "arm64":
25468
+ return "arm64";
25469
+ case "arm":
25470
+ return "arm";
25471
+ default:
25199
25472
  throw new UserError(
25200
- `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.
25473
+ `Unsupported architecture for cloudflared: ${nodeArch}
25201
25474
 
25202
- Expected: ${expectedChecksum}
25203
- Actual: ${actualSha256}`,
25204
- { telemetryMessage: "tunnel cloudflared binary checksum mismatch" }
25475
+ cloudflared supports: x64 (amd64), arm64, arm
25476
+
25477
+ You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.
25478
+ Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25479
+ { telemetryMessage: "tunnel cloudflared unsupported architecture" }
25205
25480
  );
25206
- }
25207
25481
  }
25208
- writeFileAtomic(binPath, buffer);
25209
25482
  }
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)) {
25483
+ __name(getGoArch, "getGoArch");
25484
+ function getGoOS() {
25485
+ switch (process.platform) {
25486
+ case "darwin":
25487
+ return "darwin";
25488
+ case "linux":
25489
+ return "linux";
25490
+ case "win32":
25491
+ return "windows";
25492
+ default:
25216
25493
  throw new UserError(
25217
- `CLOUDFLARED_PATH is set to "${envPath}" but the file does not exist.
25494
+ `Unsupported platform for cloudflared: ${process.platform}
25218
25495
 
25219
- Please ensure the path points to a valid cloudflared binary.`,
25220
- { telemetryMessage: "tunnel cloudflared env path missing" }
25496
+ cloudflared supports: darwin (macOS), linux, win32 (Windows)
25497
+
25498
+ You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.
25499
+ Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25500
+ { telemetryMessage: "tunnel cloudflared unsupported platform" }
25221
25501
  );
25222
- }
25223
- logger?.debug(`Using cloudflared from CLOUDFLARED_PATH: ${envPath}`);
25224
- return envPath;
25225
25502
  }
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;
25503
+ }
25504
+ __name(getGoOS, "getGoOS");
25505
+ var GITHUB_RELEASE_BASE = "https://github.com/cloudflare/cloudflared/releases/download";
25506
+ function getAssetFilename(goOS, goArch) {
25507
+ if (goOS === "windows") {
25508
+ return `cloudflared-${goOS}-${goArch}.exe`;
25233
25509
  }
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
- }
25510
+ if (goOS === "darwin") {
25511
+ return `cloudflared-${goOS}-${goArch}.tgz`;
25248
25512
  }
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" }
25513
+ return `cloudflared-${goOS}-${goArch}`;
25514
+ }
25515
+ __name(getAssetFilename, "getAssetFilename");
25516
+ async function queryUpdateService(goOS, goArch, options) {
25517
+ const { logger } = options ?? {};
25518
+ const url2 = new URL(UPDATE_SERVICE_URL);
25519
+ url2.searchParams.set("os", goOS);
25520
+ url2.searchParams.set("arch", goArch);
25521
+ logger?.debug(`Checking for latest cloudflared: ${url2.toString()}`);
25522
+ let response;
25523
+ try {
25524
+ response = await fetch(url2.toString(), {
25525
+ headers: { "User-Agent": "wrangler" }
25526
+ });
25527
+ } catch (e2) {
25528
+ logger?.debug(
25529
+ `Failed to reach update service: ${e2 instanceof Error ? e2.message : String(e2)}`
25261
25530
  );
25531
+ return null;
25262
25532
  }
25263
- if (existsSync(binPath)) {
25264
- const cacheDir = removeCloudflaredCache(versionInfo.version);
25265
- if (cacheDir) {
25266
- logger?.log(`Removed cloudflared cache: ${cacheDir}`);
25267
- }
25533
+ if (!response.ok) {
25534
+ logger?.debug(
25535
+ `Update service returned ${response.status} for ${goOS}/${goArch}`
25536
+ );
25537
+ return null;
25538
+ }
25539
+ let data;
25540
+ try {
25541
+ data = await response.json();
25542
+ } catch (e2) {
25543
+ logger?.debug(
25544
+ `Update service returned non-JSON response: ${e2 instanceof Error ? e2.message : String(e2)}`
25545
+ );
25546
+ return null;
25547
+ }
25548
+ if (typeof data.version === "string" && !CLOUDFLARED_VERSION_PATTERN.test(data.version)) {
25549
+ throw new Error(
25550
+ `[cloudflared] Invalid cloudflared version returned by update service: ${data.version}`
25551
+ );
25552
+ }
25553
+ if (data.error || !data.url || !data.version) {
25554
+ return data.version ? data : null;
25555
+ }
25556
+ return data;
25557
+ }
25558
+ __name(queryUpdateService, "queryUpdateService");
25559
+ async function getLatestVersionInfo(options) {
25560
+ const { logger } = options ?? {};
25561
+ const goOS = getGoOS();
25562
+ const goArch = getGoArch();
25563
+ const primary = await queryUpdateService(goOS, goArch, { logger });
25564
+ if (primary && primary.url && primary.version) {
25565
+ return primary;
25268
25566
  }
25269
- await downloadCloudflared(versionInfo, binPath, { logger });
25270
- validateBinary(binPath, { logger });
25271
- return binPath;
25272
- }
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
- });
25281
25567
  logger?.debug(
25282
- `Spawning cloudflared: ${binPath} ${redactCloudflaredArgsForLogging(args).join(" ")}`
25568
+ `Update worker had no result for ${goOS}/${goArch}, falling back to GitHub release URL`
25283
25569
  );
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;
25570
+ const fallback = await queryUpdateService("linux", "amd64", { logger });
25571
+ if (!fallback?.version) {
25572
+ throw new UserError(
25573
+ `[cloudflared] Failed to determine the latest cloudflared version.
25574
+
25575
+ The update service did not return results for ${goOS}/${goArch},
25576
+ and the fallback query also failed.
25577
+
25578
+ You can manually install cloudflared from:
25579
+ https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25580
+ { telemetryMessage: "tunnel cloudflared version lookup failed" }
25581
+ );
25582
+ }
25583
+ const version2 = fallback.version;
25584
+ const filename = getAssetFilename(goOS, goArch);
25585
+ const url2 = `${GITHUB_RELEASE_BASE}/${version2}/${filename}`;
25586
+ const compressed = filename.endsWith(".tgz");
25587
+ return {
25588
+ url: url2,
25589
+ version: version2,
25590
+ checksum: "",
25591
+ // no checksum available for fallback URLs
25592
+ compressed,
25593
+ shouldUpdate: true,
25594
+ userMessage: "",
25595
+ error: ""
25596
+ };
25289
25597
  }
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;
25598
+ __name(getLatestVersionInfo, "getLatestVersionInfo");
25599
+ function getCacheDir(version2) {
25600
+ return join(getGlobalConfigPath(), "cloudflared", version2);
25601
+ }
25602
+ __name(getCacheDir, "getCacheDir");
25603
+ function getCloudflaredBinPath(version2) {
25604
+ const binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
25605
+ return join(getCacheDir(version2), binName);
25606
+ }
25607
+ __name(getCloudflaredBinPath, "getCloudflaredBinPath");
25608
+ function isBinaryExecutable(binPath) {
25609
+ try {
25610
+ accessSync(binPath, constants.X_OK);
25611
+ return true;
25612
+ } catch {
25613
+ return false;
25296
25614
  }
25297
- return null;
25298
25615
  }
25299
- __name(removeCloudflaredCache, "removeCloudflaredCache");
25616
+ __name(isBinaryExecutable, "isBinaryExecutable");
25617
+ function validateBinary(binPath, options) {
25618
+ const { logger } = options ?? {};
25619
+ try {
25620
+ const output = execFileSync(binPath, ["--version"], {
25621
+ stdio: ["pipe", "pipe", "pipe"],
25622
+ timeout: 1e4,
25623
+ encoding: "utf8"
25624
+ }).trim();
25625
+ logger?.debug(`cloudflared version: ${output}`);
25626
+ } catch {
25627
+ let errorMessage = `[cloudflared] Failed to validate cloudflared binary at ${binPath}
25300
25628
 
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" };
25629
+ `;
25630
+ errorMessage += `This usually means:
25631
+ `;
25632
+ errorMessage += ` - The binary is corrupted or incomplete
25633
+ `;
25634
+ errorMessage += ` - You're missing required system libraries
25635
+ `;
25636
+ if (process.platform === "linux") {
25637
+ errorMessage += `
25638
+ On Linux, make sure you have the required dependencies:
25639
+ `;
25640
+ errorMessage += ` - glibc (GNU C Library)
25641
+ `;
25642
+ errorMessage += ` - For Debian/Ubuntu: sudo apt-get install libc6
25643
+ `;
25339
25644
  }
25340
- return waitForQuickTunnelReady(process4, timeoutMs, {
25341
- logger,
25342
- origin: options.origin
25645
+ const cacheDir = join(getGlobalConfigPath(), "cloudflared");
25646
+ errorMessage += `
25647
+ You can try:
25648
+ `;
25649
+ errorMessage += ` 1. Deleting the cache directory: rm -rf ${cacheDir}
25650
+ `;
25651
+ errorMessage += ` 2. Running the command again to re-download
25652
+ `;
25653
+ errorMessage += ` 3. Manually installing cloudflared: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/
25654
+ `;
25655
+ errorMessage += ` 4. Setting CLOUDFLARED_PATH to point to your cloudflared binary`;
25656
+ throw new UserError(errorMessage, {
25657
+ telemetryMessage: "tunnel cloudflared validation failed"
25343
25658
  });
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
25659
  }
25359
- __name(disposeTunnel, "disposeTunnel");
25360
- function clearTunnelTimers() {
25361
- if (expiryTimeout) {
25362
- clearTimeout(expiryTimeout);
25363
- expiryTimeout = void 0;
25660
+ }
25661
+ __name(validateBinary, "validateBinary");
25662
+ function redactCloudflaredArgsForLogging(args) {
25663
+ const redacted = [...args];
25664
+ for (let i = 0; i < redacted.length; i++) {
25665
+ const arg = redacted[i];
25666
+ if (arg === "--token" && i + 1 < redacted.length) {
25667
+ redacted[i + 1] = "[REDACTED]";
25364
25668
  }
25365
- if (reminderInterval) {
25366
- clearInterval(reminderInterval);
25367
- reminderInterval = void 0;
25669
+ if (arg.startsWith("--token=")) {
25670
+ redacted[i] = "--token=[REDACTED]";
25368
25671
  }
25369
25672
  }
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
- }
25673
+ return redacted;
25674
+ }
25675
+ __name(redactCloudflaredArgsForLogging, "redactCloudflaredArgsForLogging");
25676
+ function tryGetCloudflaredFromPath(options) {
25677
+ const { logger } = options ?? {};
25678
+ if (!(0, import_command_exists.sync)("cloudflared")) {
25679
+ return null;
25387
25680
  }
25388
- __name(scheduleReminder, "scheduleReminder");
25389
- function scheduleExpiryTimeout() {
25390
- if (disposed) {
25681
+ try {
25682
+ validateBinary("cloudflared", { logger });
25683
+ return "cloudflared";
25684
+ } catch (e2) {
25685
+ logger?.debug("cloudflared found in PATH but failed validation", e2);
25686
+ return null;
25687
+ }
25688
+ }
25689
+ __name(tryGetCloudflaredFromPath, "tryGetCloudflaredFromPath");
25690
+ function getInstalledVersion(binPath) {
25691
+ try {
25692
+ const output = execFileSync(binPath, ["--version"], {
25693
+ stdio: ["pipe", "pipe", "pipe"],
25694
+ timeout: 1e4,
25695
+ encoding: "utf8"
25696
+ });
25697
+ const match = output.match(/(\d+\.\d+\.\d+)/);
25698
+ return match ? match[1] : null;
25699
+ } catch {
25700
+ return null;
25701
+ }
25702
+ }
25703
+ __name(getInstalledVersion, "getInstalledVersion");
25704
+ function isVersionOutdated(installed, latest) {
25705
+ const parse3 = /* @__PURE__ */ __name((v) => v.split(".").map(Number), "parse");
25706
+ const [iYear, iMonth, iPatch] = parse3(installed);
25707
+ const [lYear, lMonth, lPatch] = parse3(latest);
25708
+ if (iYear !== lYear) {
25709
+ return iYear < lYear;
25710
+ }
25711
+ if (iMonth !== lMonth) {
25712
+ return iMonth < lMonth;
25713
+ }
25714
+ return iPatch < lPatch;
25715
+ }
25716
+ __name(isVersionOutdated, "isVersionOutdated");
25717
+ async function warnIfOutdated(binPath, options) {
25718
+ const { logger } = options ?? {};
25719
+ try {
25720
+ const installed = getInstalledVersion(binPath);
25721
+ if (!installed) {
25391
25722
  return;
25392
25723
  }
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) {
25724
+ const latest = await queryUpdateService(getGoOS(), getGoArch(), { logger }) ?? await queryUpdateService("linux", "amd64", { logger });
25725
+ const latestVersion = latest?.version;
25726
+ if (!latestVersion) {
25411
25727
  return;
25412
25728
  }
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))}.`
25729
+ if (isVersionOutdated(installed, latestVersion)) {
25730
+ logger?.warn(
25731
+ `Your cloudflared (${installed}) is outdated. Latest version is ${latestVersion}.
25732
+ Update: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`
25423
25733
  );
25424
- scheduleExpiryTimeout();
25425
- return;
25426
25734
  }
25427
- logger?.log(
25428
- `Tunnel expiry extended by ${formatTunnelDuration(extendedByMs)}. It now expires at ${timeFormatter.format(new Date(expiresAt))}.`
25429
- );
25430
- scheduleExpiryTimeout();
25735
+ } catch (e2) {
25736
+ logger?.debug("Failed to check cloudflared version", e2);
25431
25737
  }
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
25738
  }
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`;
25739
+ __name(warnIfOutdated, "warnIfOutdated");
25740
+ function writeFileAtomic(filePath, contents) {
25741
+ const dir2 = dirname(filePath);
25742
+ const tmpPath = join(
25743
+ dir2,
25744
+ `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
25745
+ );
25746
+ try {
25747
+ writeFileSync(tmpPath, contents);
25748
+ renameSync(tmpPath, filePath);
25749
+ } finally {
25750
+ try {
25751
+ if (existsSync(tmpPath)) {
25752
+ unlinkSync(tmpPath);
25753
+ }
25754
+ } catch {
25755
+ }
25450
25756
  }
25451
- return `${hours}h ${minutes}m`;
25452
25757
  }
25453
- __name(formatTunnelDuration, "formatTunnelDuration");
25454
- function terminateCloudflared(cloudflared) {
25455
- if (cloudflared.killed) {
25456
- return;
25758
+ __name(writeFileAtomic, "writeFileAtomic");
25759
+ async function downloadCloudflared(versionInfo, binPath, options) {
25760
+ const { logger } = options ?? {};
25761
+ const { url: url2, version: version2, checksum, compressed } = versionInfo;
25762
+ logger?.log(`Downloading cloudflared ${version2}...`);
25763
+ logger?.debug(`Download URL: ${url2}`);
25764
+ const cacheDir = dirname(binPath);
25765
+ mkdirSync(cacheDir, { recursive: true });
25766
+ let response;
25767
+ try {
25768
+ response = await fetch(url2, {
25769
+ headers: { "User-Agent": "wrangler" }
25770
+ });
25771
+ } catch (e2) {
25772
+ throw new UserError(
25773
+ `[cloudflared] Failed to download cloudflared from ${url2}
25774
+
25775
+ Network error: ${e2 instanceof Error ? e2.message : String(e2)}
25776
+
25777
+ Please check your internet connection and try again.
25778
+ If you're behind a proxy, make sure it's configured correctly.`,
25779
+ { telemetryMessage: "tunnel cloudflared download network failed" }
25780
+ );
25457
25781
  }
25458
- cloudflared.unref();
25459
- cloudflared.kill("SIGTERM");
25460
- const forceKillTimer = setTimeout(() => {
25461
- if (!cloudflared.killed) {
25462
- cloudflared.kill("SIGKILL");
25782
+ if (!response.ok) {
25783
+ throw new UserError(
25784
+ `[cloudflared] Failed to download cloudflared from ${url2}
25785
+
25786
+ HTTP ${response.status}: ${response.statusText}
25787
+
25788
+ You can manually download cloudflared from:
25789
+ https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/`,
25790
+ { telemetryMessage: "tunnel cloudflared download response failed" }
25791
+ );
25792
+ }
25793
+ try {
25794
+ if (compressed) {
25795
+ await downloadAndExtractTarball(response, checksum, binPath, cacheDir);
25796
+ } else {
25797
+ await downloadBinary(response, checksum, binPath);
25463
25798
  }
25464
- }, TUNNEL_FORCE_KILL_TIMEOUT_MS);
25465
- forceKillTimer.unref();
25799
+ } catch (e2) {
25800
+ try {
25801
+ if (existsSync(binPath)) {
25802
+ unlinkSync(binPath);
25803
+ }
25804
+ } catch {
25805
+ }
25806
+ if (e2 instanceof UserError) {
25807
+ throw e2;
25808
+ }
25809
+ throw new UserError(
25810
+ `[cloudflared] Failed to save cloudflared binary
25811
+
25812
+ Error: ${e2 instanceof Error ? e2.message : String(e2)}
25813
+
25814
+ Please ensure you have write permissions to: ${cacheDir}`,
25815
+ { telemetryMessage: "tunnel cloudflared save failed" }
25816
+ );
25817
+ }
25818
+ if (process.platform !== "win32") {
25819
+ chmodSync(binPath, 493);
25820
+ }
25821
+ logger?.log(`cloudflared ${version2} installed`);
25822
+ logger?.debug(`Binary location: ${binPath}`);
25466
25823
  }
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
- )
25824
+ __name(downloadCloudflared, "downloadCloudflared");
25825
+ async function downloadAndExtractTarball(response, expectedChecksum, binPath, cacheDir) {
25826
+ const tempTarPath = join(cacheDir, "cloudflared.tgz");
25827
+ const buffer = Buffer.from(await response.arrayBuffer());
25828
+ writeFileSync(tempTarPath, buffer);
25829
+ try {
25830
+ execFileSync("tar", ["-xzf", tempTarPath, "-C", cacheDir], {
25831
+ stdio: "ignore"
25832
+ });
25833
+ const extractedPath = join(cacheDir, "cloudflared");
25834
+ if (extractedPath !== binPath && existsSync(extractedPath)) {
25835
+ renameSync(extractedPath, binPath);
25836
+ }
25837
+ if (expectedChecksum) {
25838
+ const extractedBinary = readFileSync$1(binPath);
25839
+ const actualSha256 = sha256Hex(extractedBinary);
25840
+ if (actualSha256 !== expectedChecksum) {
25841
+ throw new UserError(
25842
+ `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.
25843
+
25844
+ Expected: ${expectedChecksum}
25845
+ Actual: ${actualSha256}`,
25846
+ {
25847
+ telemetryMessage: "tunnel cloudflared extracted binary checksum mismatch"
25848
+ }
25484
25849
  );
25485
25850
  }
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
25851
  }
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
- );
25852
+ } finally {
25853
+ try {
25854
+ if (existsSync(tempTarPath)) {
25855
+ unlinkSync(tempTarPath);
25520
25856
  }
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 });
25857
+ } catch {
25858
+ }
25537
25859
  }
25538
- return new Error(errorMessage);
25539
25860
  }
25540
- __name(createTunnelStartupError, "createTunnelStartupError");
25861
+ __name(downloadAndExtractTarball, "downloadAndExtractTarball");
25862
+ async function downloadBinary(response, expectedChecksum, binPath) {
25863
+ const buffer = Buffer.from(await response.arrayBuffer());
25864
+ if (expectedChecksum) {
25865
+ const actualSha256 = sha256Hex(buffer);
25866
+ if (actualSha256 !== expectedChecksum) {
25867
+ throw new UserError(
25868
+ `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.
25541
25869
 
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
- );
25870
+ Expected: ${expectedChecksum}
25871
+ Actual: ${actualSha256}`,
25872
+ { telemetryMessage: "tunnel cloudflared binary checksum mismatch" }
25873
+ );
25874
+ }
25557
25875
  }
25876
+ writeFileAtomic(binPath, buffer);
25558
25877
  }
25559
- __name(maybeThrowFriendlyError, "maybeThrowFriendlyError");
25878
+ __name(downloadBinary, "downloadBinary");
25879
+ async function getCloudflaredPath(options) {
25880
+ const logger = options?.logger;
25881
+ const envPath = getCloudflaredPathFromEnv();
25882
+ if (envPath) {
25883
+ if (!existsSync(envPath)) {
25884
+ throw new UserError(
25885
+ `CLOUDFLARED_PATH is set to "${envPath}" but the file does not exist.
25560
25886
 
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
- );
25887
+ Please ensure the path points to a valid cloudflared binary.`,
25888
+ { telemetryMessage: "tunnel cloudflared env path missing" }
25889
+ );
25890
+ }
25891
+ logger?.debug(`Using cloudflared from CLOUDFLARED_PATH: ${envPath}`);
25892
+ return envPath;
25596
25893
  }
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
25894
+ const pathBin = tryGetCloudflaredFromPath({ logger });
25895
+ if (pathBin) {
25896
+ logger?.debug("Using cloudflared from PATH");
25897
+ if (!options?.skipVersionCheck) {
25898
+ await warnIfOutdated(pathBin, { logger });
25605
25899
  }
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
- };
25900
+ return pathBin;
25642
25901
  }
25643
- if (isWAFBlockResponse(response.headers)) {
25644
- throwWAFBlockError(
25645
- response.headers,
25646
- method,
25647
- resource,
25648
- response.status,
25649
- response.statusText,
25650
- retryAfterMs
25902
+ const versionInfo = await getLatestVersionInfo({ logger });
25903
+ const binPath = getCloudflaredBinPath(versionInfo.version);
25904
+ let needsDownload = !isBinaryExecutable(binPath);
25905
+ if (!needsDownload) {
25906
+ try {
25907
+ validateBinary(binPath, { logger });
25908
+ logger?.debug(
25909
+ `Using cached cloudflared ${versionInfo.version}: ${binPath}`
25910
+ );
25911
+ return binPath;
25912
+ } catch (e2) {
25913
+ logger?.debug("Cached cloudflared failed validation; re-downloading", e2);
25914
+ needsDownload = true;
25915
+ }
25916
+ }
25917
+ const shouldDownload = await options?.confirmDownload?.(
25918
+ `cloudflared (${versionInfo.version}) is needed but not installed. Download to ${binPath}?`
25919
+ ) ?? true;
25920
+ if (!shouldDownload) {
25921
+ throw new UserError(
25922
+ `cloudflared is required to run this command.
25923
+
25924
+ You can install it manually from:
25925
+ https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/
25926
+
25927
+ Then either add it to your PATH or set CLOUDFLARED_PATH.`,
25928
+ { telemetryMessage: "tunnel cloudflared download declined" }
25651
25929
  );
25652
25930
  }
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
- });
25931
+ if (existsSync(binPath)) {
25932
+ const cacheDir = removeCloudflaredCache(versionInfo.version);
25933
+ if (cacheDir) {
25934
+ logger?.log(`Removed cloudflared cache: ${cacheDir}`);
25935
+ }
25936
+ }
25937
+ await downloadCloudflared(versionInfo, binPath, { logger });
25938
+ validateBinary(binPath, { logger });
25939
+ return binPath;
25940
+ }
25941
+ __name(getCloudflaredPath, "getCloudflaredPath");
25942
+ async function spawnCloudflared(args, options) {
25943
+ const logger = options?.logger;
25944
+ const binPath = await getCloudflaredPath({
25945
+ skipVersionCheck: options?.skipVersionCheck,
25946
+ confirmDownload: options?.confirmDownload,
25947
+ logger
25948
+ });
25949
+ logger?.debug(
25950
+ `Spawning cloudflared: ${binPath} ${redactCloudflaredArgsForLogging(args).join(" ")}`
25951
+ );
25952
+ const cloudflared = spawn(binPath, args, {
25953
+ stdio: options?.stdio ?? "inherit",
25954
+ env: options?.env ? { ...process.env, ...options.env } : void 0
25955
+ });
25956
+ return cloudflared;
25957
+ }
25958
+ __name(spawnCloudflared, "spawnCloudflared");
25959
+ function removeCloudflaredCache(version2) {
25960
+ const cacheDir = version2 ? getCacheDir(version2) : join(getGlobalConfigPath(), "cloudflared");
25961
+ if (existsSync(cacheDir)) {
25962
+ removeDirSync(cacheDir);
25963
+ return cacheDir;
25673
25964
  }
25965
+ return null;
25674
25966
  }
25675
- __name(fetchInternalBase, "fetchInternalBase");
25676
- async function fetchResultBase(complianceConfig, resource, init = {}, userAgent, logger, queryParams, abortSignal, credentials) {
25967
+ __name(removeCloudflaredCache, "removeCloudflaredCache");
25968
+
25969
+ // src/tunnel.ts
25970
+ var TUNNEL_STARTUP_TIMEOUT_MS = 3e4;
25971
+ var TUNNEL_API_TIMEOUT_MS = 6e4;
25972
+ var TUNNEL_FORCE_KILL_TIMEOUT_MS = 5e3;
25973
+ var DEFAULT_TUNNEL_EXPIRY_MS = 60 * 60 * 1e3;
25974
+ var DEFAULT_TUNNEL_EXTENSION_MS = 60 * 60 * 1e3;
25975
+ var DEFAULT_TUNNEL_MAX_REMAINING_MS = 3 * 60 * 60 * 1e3;
25976
+ var DEFAULT_TUNNEL_REMINDER_INTERVAL_MS = 10 * 60 * 1e3;
25977
+ var QUICK_TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
25978
+ var LOCAL_TUNNEL_HOSTNAMES = /* @__PURE__ */ new Set([
25979
+ "localhost",
25980
+ "127.0.0.1",
25981
+ "::1",
25982
+ "0.0.0.0",
25983
+ "::"
25984
+ ]);
25985
+ async function resolveNamedTunnel(name, origin, options) {
25677
25986
  const {
25678
- response: json2,
25679
- status,
25680
- retryAfterMs
25681
- } = await fetchInternalBase(
25987
+ accountId,
25988
+ apiToken,
25989
+ complianceRegion,
25990
+ logger,
25991
+ userAgent,
25992
+ abortSignal
25993
+ } = options;
25994
+ const complianceConfig = { compliance_region: complianceRegion };
25995
+ const resource = `/accounts/${accountId}/cfd_tunnel`;
25996
+ const tunnels = await fetchResultBase(
25682
25997
  complianceConfig,
25683
25998
  resource,
25684
- init,
25999
+ void 0,
25685
26000
  userAgent,
25686
26001
  logger,
25687
- queryParams,
25688
- abortSignal,
25689
- credentials
26002
+ new URLSearchParams({ name, is_deleted: "false" }),
26003
+ createApiAbortSignal(TUNNEL_API_TIMEOUT_MS, abortSignal),
26004
+ apiToken
25690
26005
  );
25691
- if (json2.success) {
25692
- return json2.result;
25693
- } else {
25694
- throwFetchError(resource, json2, status, retryAfterMs);
26006
+ const tunnel = tunnels.find((item) => item.name === name);
26007
+ if (!tunnel) {
26008
+ throw new UserError(
26009
+ `No Cloudflare Tunnel named "${name}" was found in this account. Use "wrangler tunnel list" to see available tunnels.`,
26010
+ { telemetryMessage: "tunnel resolve named missing tunnel" }
26011
+ );
25695
26012
  }
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
26013
+ const tunnelId = tunnel.id;
26014
+ if (!tunnelId) {
26015
+ throw new FatalError(
26016
+ `Tunnel "${name}" was found but has no ID. This is unexpected.`,
26017
+ { telemetryMessage: "tunnel resolve named missing tunnel id" }
25720
26018
  );
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
26019
  }
25732
- return results;
25733
- }
25734
- __name(fetchListResultBase, "fetchListResultBase");
25735
- function truncate(text, maxLength) {
25736
- const { length } = text;
25737
- if (length <= maxLength) {
25738
- return text;
26020
+ const configuration = await fetchResultBase(
26021
+ complianceConfig,
26022
+ `${resource}/${tunnelId}/configurations`,
26023
+ void 0,
26024
+ userAgent,
26025
+ logger,
26026
+ void 0,
26027
+ createApiAbortSignal(TUNNEL_API_TIMEOUT_MS, abortSignal),
26028
+ apiToken
26029
+ );
26030
+ const ingress = configuration.config?.ingress ?? [];
26031
+ const hostnames = getMatchingIngressHostnames(origin, ingress);
26032
+ if (hostnames.length === 0) {
26033
+ throw new UserError(
26034
+ createMissingIngressMessage(name, origin, {
26035
+ dashboardUrl: `https://dash.cloudflare.com/${accountId}/tunnels/${tunnelId}`,
26036
+ ingress
26037
+ }),
26038
+ { telemetryMessage: "tunnel resolve named ingress mismatch" }
26039
+ );
25739
26040
  }
25740
- return `${text.substring(0, maxLength)}... (length = ${length})`;
25741
- }
25742
- __name(truncate, "truncate");
25743
- function isWAFBlockResponse(headers) {
25744
- return headers.get("cf-mitigated") === "challenge";
26041
+ const token = await fetchResultBase(
26042
+ complianceConfig,
26043
+ `${resource}/${tunnelId}/token`,
26044
+ void 0,
26045
+ userAgent,
26046
+ logger,
26047
+ void 0,
26048
+ createApiAbortSignal(TUNNEL_API_TIMEOUT_MS, abortSignal),
26049
+ apiToken
26050
+ );
26051
+ return { hostnames, token: String(token) };
25745
26052
  }
25746
- __name(isWAFBlockResponse, "isWAFBlockResponse");
25747
- function parseRetryAfterValue(retryAfter) {
25748
- if (!retryAfter) {
25749
- return void 0;
26053
+ __name(resolveNamedTunnel, "resolveNamedTunnel");
26054
+ function getMatchingIngressHostnames(origin, ingressConfig) {
26055
+ const hostnames = /* @__PURE__ */ new Set();
26056
+ const originUrl = normalizeURL(origin);
26057
+ for (const ingress of ingressConfig) {
26058
+ try {
26059
+ const serviceUrl = normalizeURL(ingress.service);
26060
+ if (ingress.hostname && serviceUrl.toString() === originUrl.toString()) {
26061
+ hostnames.add(ingress.hostname);
26062
+ }
26063
+ } catch {
26064
+ }
25750
26065
  }
25751
- if (/^\d+$/.test(retryAfter.trim())) {
25752
- return Number(retryAfter) * 1e3;
26066
+ return [...hostnames];
26067
+ }
26068
+ __name(getMatchingIngressHostnames, "getMatchingIngressHostnames");
26069
+ function normalizeURL(url2) {
26070
+ const normalizedUrl = new URL(url2);
26071
+ if (LOCAL_TUNNEL_HOSTNAMES.has(normalizedUrl.hostname)) {
26072
+ normalizedUrl.hostname = "localhost";
25753
26073
  }
25754
- const retryAfterDate = new Date(retryAfter);
25755
- if (!Number.isNaN(retryAfterDate.getTime())) {
25756
- return Math.max(0, retryAfterDate.getTime() - Date.now());
26074
+ if (!normalizedUrl.port) {
26075
+ switch (normalizedUrl.protocol) {
26076
+ case "http:":
26077
+ normalizedUrl.port = "80";
26078
+ break;
26079
+ case "https:":
26080
+ normalizedUrl.port = "443";
26081
+ break;
26082
+ }
25757
26083
  }
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];
26084
+ return normalizedUrl;
25773
26085
  }
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;
26086
+ __name(normalizeURL, "normalizeURL");
26087
+ function createApiAbortSignal(timeoutMs, abortSignal) {
26088
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
26089
+ return abortSignal ? AbortSignal.any([abortSignal, timeoutSignal]) : timeoutSignal;
25780
26090
  }
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;
26091
+ __name(createApiAbortSignal, "createApiAbortSignal");
26092
+ function createMissingIngressMessage(name, origin, {
26093
+ dashboardUrl,
26094
+ ingress
26095
+ }) {
26096
+ if (ingress.length === 0) {
26097
+ return [
26098
+ `Tunnel "${name}" has no routes configured.`,
26099
+ "",
26100
+ `Add a route for ${origin} in the Cloudflare dashboard:`,
26101
+ dashboardUrl,
26102
+ ""
26103
+ ].join("\n");
26104
+ }
26105
+ return [
26106
+ `Tunnel "${name}" has no route for ${origin}`,
26107
+ "",
26108
+ "Resolved routes:",
26109
+ ...ingress.map(
26110
+ ({ hostname: hostname3, service }) => ` - ${hostname3 ?? "(no hostname)"} -> ${service}`
26111
+ ),
26112
+ "",
26113
+ "Update your local server settings or the tunnel routes in the Cloudflare dashboard:",
26114
+ dashboardUrl,
26115
+ ""
26116
+ ].join("\n");
25792
26117
  }
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);
26118
+ __name(createMissingIngressMessage, "createMissingIngressMessage");
26119
+ function startTunnel(options) {
26120
+ let disposed = false;
26121
+ let reminderInterval;
26122
+ let expiryTimeout;
26123
+ let expiresAt = 0;
26124
+ let cloudflaredProcess;
26125
+ const logger = options.logger;
26126
+ const timeoutMs = options.timeoutMs ?? TUNNEL_STARTUP_TIMEOUT_MS;
26127
+ const reminderIntervalMs = options.reminderIntervalMs ?? DEFAULT_TUNNEL_REMINDER_INTERVAL_MS;
26128
+ const defaultExpiryMs = options.expiryMs ?? DEFAULT_TUNNEL_EXPIRY_MS;
26129
+ const isNamedTunnel = options.token !== void 0;
26130
+ const timeFormatter = new Intl.DateTimeFormat(void 0, {
26131
+ timeStyle: "short"
26132
+ });
26133
+ const cloudflaredArgs = isNamedTunnel ? ["tunnel", "--no-autoupdate", "run"] : ["tunnel", "--no-autoupdate", "--url", options.origin.href];
26134
+ const cloudflaredPromise = spawnCloudflared(cloudflaredArgs, {
26135
+ stdio: "pipe",
26136
+ env: options.token ? { TUNNEL_TOKEN: options.token } : void 0,
26137
+ skipVersionCheck: true,
26138
+ logger
26139
+ }).then((process4) => {
26140
+ cloudflaredProcess = process4;
26141
+ if (disposed) {
26142
+ terminateCloudflared(process4);
26143
+ }
26144
+ return process4;
26145
+ });
26146
+ const readyPromise = cloudflaredPromise.then((process4) => {
26147
+ if (isNamedTunnel) {
26148
+ return { mode: "named" };
26149
+ }
26150
+ return waitForQuickTunnelReady(process4, timeoutMs, {
26151
+ logger,
26152
+ origin: options.origin
26153
+ });
26154
+ }).then((result) => {
26155
+ expiresAt = Date.now() + defaultExpiryMs;
26156
+ scheduleExpiryTimeout();
26157
+ scheduleReminder(
26158
+ result.mode === "quick" ? result.publicUrl.origin : void 0
26159
+ );
26160
+ return result;
26161
+ });
26162
+ function disposeTunnel() {
26163
+ disposed = true;
26164
+ clearTunnelTimers();
26165
+ if (cloudflaredProcess) {
26166
+ terminateCloudflared(cloudflaredProcess);
25803
26167
  }
25804
26168
  }
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"
26169
+ __name(disposeTunnel, "disposeTunnel");
26170
+ function clearTunnelTimers() {
26171
+ if (expiryTimeout) {
26172
+ clearTimeout(expiryTimeout);
26173
+ expiryTimeout = void 0;
26174
+ }
26175
+ if (reminderInterval) {
26176
+ clearInterval(reminderInterval);
26177
+ reminderInterval = void 0;
26178
+ }
26179
+ }
26180
+ __name(clearTunnelTimers, "clearTunnelTimers");
26181
+ function scheduleReminder(publicURL) {
26182
+ if (reminderIntervalMs > 0) {
26183
+ reminderInterval = setInterval(() => {
26184
+ if (disposed) {
26185
+ return;
26186
+ }
26187
+ const remainingMs = expiresAt - Date.now();
26188
+ if (remainingMs <= 0) {
26189
+ return;
26190
+ }
26191
+ logger?.log(
26192
+ `${publicURL ? `Tunnel still open, expires in ${formatTunnelDuration(remainingMs)}: ${publicURL}` : `The tunnel is still open. It expires in ${formatTunnelDuration(remainingMs)}.`}${options.extendHint ? ` ${options.extendHint}` : ""}`
26193
+ );
26194
+ }, reminderIntervalMs);
26195
+ reminderInterval.unref?.();
26196
+ }
26197
+ }
26198
+ __name(scheduleReminder, "scheduleReminder");
26199
+ function scheduleExpiryTimeout() {
26200
+ if (disposed) {
26201
+ return;
26202
+ }
26203
+ if (expiryTimeout) {
26204
+ clearTimeout(expiryTimeout);
26205
+ }
26206
+ expiryTimeout = setTimeout(
26207
+ () => {
26208
+ if (disposed) {
26209
+ return;
25815
26210
  }
26211
+ logger?.log("Tunnel expired. Closing tunnel.");
26212
+ disposeTunnel();
26213
+ },
26214
+ Math.max(0, expiresAt - Date.now())
26215
+ );
26216
+ expiryTimeout.unref();
26217
+ }
26218
+ __name(scheduleExpiryTimeout, "scheduleExpiryTimeout");
26219
+ function extendExpiry(ms = DEFAULT_TUNNEL_EXTENSION_MS) {
26220
+ if (disposed || !expiryTimeout || ms <= 0) {
26221
+ return;
26222
+ }
26223
+ const now = Date.now();
26224
+ const previousExpiresAt = expiresAt;
26225
+ expiresAt = Math.min(
26226
+ now + DEFAULT_TUNNEL_MAX_REMAINING_MS,
26227
+ Math.max(expiresAt, now) + ms
26228
+ );
26229
+ const extendedByMs = expiresAt - previousExpiresAt;
26230
+ if (extendedByMs < ms) {
26231
+ logger?.log(
26232
+ `Tunnel expiry extended to the ${formatTunnelDuration(DEFAULT_TUNNEL_MAX_REMAINING_MS)} limit. It now expires at ${timeFormatter.format(new Date(expiresAt))}.`
25816
26233
  );
26234
+ scheduleExpiryTimeout();
26235
+ return;
25817
26236
  }
26237
+ logger?.log(
26238
+ `Tunnel expiry extended by ${formatTunnelDuration(extendedByMs)}. It now expires at ${timeFormatter.format(new Date(expiresAt))}.`
26239
+ );
26240
+ scheduleExpiryTimeout();
25818
26241
  }
26242
+ __name(extendExpiry, "extendExpiry");
26243
+ return {
26244
+ ready: /* @__PURE__ */ __name(() => readyPromise, "ready"),
26245
+ isOpen: /* @__PURE__ */ __name(() => !disposed, "isOpen"),
26246
+ dispose: disposeTunnel,
26247
+ extendExpiry
26248
+ };
25819
26249
  }
25820
- __name(validateAuthorizationHeaderValue, "validateAuthorizationHeaderValue");
25821
- function formatAuthorizationHeaderCharacter(character, codePoint) {
25822
- if (codePoint === void 0) {
25823
- return '"\\u{unknown}"';
26250
+ __name(startTunnel, "startTunnel");
26251
+ function formatTunnelDuration(durationMs) {
26252
+ const totalMinutes = Math.max(1, Math.ceil(durationMs / 6e4));
26253
+ const hours = Math.floor(totalMinutes / 60);
26254
+ const minutes = totalMinutes % 60;
26255
+ if (hours === 0) {
26256
+ return `${minutes}m`;
25824
26257
  }
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);
26258
+ if (minutes === 0) {
26259
+ return `${hours}h`;
26260
+ }
26261
+ return `${hours}h ${minutes}m`;
25832
26262
  }
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 "";
26263
+ __name(formatTunnelDuration, "formatTunnelDuration");
26264
+ function terminateCloudflared(cloudflared) {
26265
+ if (cloudflared.killed) {
26266
+ return;
26267
+ }
26268
+ cloudflared.unref();
26269
+ cloudflared.kill("SIGTERM");
26270
+ const forceKillTimer = setTimeout(() => {
26271
+ if (!cloudflared.killed) {
26272
+ cloudflared.kill("SIGKILL");
25839
26273
  }
25840
- return codePoint <= 65535 ? `\\u${codePoint.toString(16).toUpperCase().padStart(4, "0")}` : `\\u{${codePoint.toString(16).toUpperCase()}}`;
25841
- }).join("");
26274
+ }, TUNNEL_FORCE_KILL_TIMEOUT_MS);
26275
+ forceKillTimer.unref();
25842
26276
  }
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 });
26277
+ __name(terminateCloudflared, "terminateCloudflared");
26278
+ function waitForQuickTunnelReady(cloudflared, timeoutMs, options) {
26279
+ return new Promise((resolve3, reject) => {
26280
+ let resolved = false;
26281
+ let stderrOutput = "";
26282
+ const logger = options?.logger;
26283
+ const origin = options?.origin;
26284
+ const timeoutId = setTimeout(() => {
26285
+ if (!resolved) {
26286
+ resolved = true;
26287
+ terminateCloudflared(cloudflared);
26288
+ reject(
26289
+ createTunnelStartupError(
26290
+ `Timed out waiting for cloudflared to start (${timeoutMs / 1e3}s).`,
26291
+ stderrOutput,
26292
+ origin
26293
+ )
26294
+ );
26295
+ }
26296
+ }, timeoutMs);
26297
+ timeoutId.unref();
26298
+ if (cloudflared.stderr) {
26299
+ cloudflared.stderr.on("data", (data) => {
26300
+ const chunk = data.toString();
26301
+ stderrOutput += chunk;
26302
+ logger?.debug("[cloudflared]", chunk.trimEnd());
26303
+ const match = QUICK_TUNNEL_URL_REGEX.exec(stderrOutput);
26304
+ if (match && !resolved) {
26305
+ resolved = true;
26306
+ clearTimeout(timeoutId);
26307
+ resolve3({ mode: "quick", publicUrl: new URL(match[0]) });
26308
+ }
26309
+ });
25860
26310
  }
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.`
26311
+ cloudflared.on("error", (error51) => {
26312
+ if (!resolved) {
26313
+ resolved = true;
26314
+ clearTimeout(timeoutId);
26315
+ reject(new Error(`Failed to start cloudflared: ${error51.message}`));
26316
+ }
25865
26317
  });
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}`
26318
+ cloudflared.on("exit", (code, signal) => {
26319
+ if (!resolved) {
26320
+ resolved = true;
26321
+ clearTimeout(timeoutId);
26322
+ const reason = signal ? `terminated by signal ${signal}` : `exited with code ${code}`;
26323
+ reject(
26324
+ createTunnelStartupError(
26325
+ `cloudflared ${reason} before the tunnel was ready.`,
26326
+ stderrOutput,
26327
+ origin
26328
+ )
26329
+ );
25903
26330
  }
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
26331
+ });
25922
26332
  });
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
26333
  }
25936
- __name(hasCursor, "hasCursor");
25937
- function maybeAddTraceHeader(headers) {
25938
- const traceHeader = getTraceHeader();
25939
- if (traceHeader) {
25940
- headers.set("Cf-Trace-Id", traceHeader);
26334
+ __name(waitForQuickTunnelReady, "waitForQuickTunnelReady");
26335
+ function createTunnelStartupError(message, stderrOutput, origin) {
26336
+ const isQuickTunnelRateLimited = stderrOutput.includes(
26337
+ "429 Too Many Requests"
26338
+ );
26339
+ const errorMessage = `${message}
26340
+ cloudflared output:
26341
+ ${stderrOutput || "(no output)"}
26342
+
26343
+ The local dev server started at ${origin.href}
26344
+ ` + (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.`);
26345
+ if (isQuickTunnelRateLimited) {
26346
+ return new UserError(errorMessage, { telemetryMessage: false });
25941
26347
  }
26348
+ return new Error(errorMessage);
25942
26349
  }
25943
- __name(maybeAddTraceHeader, "maybeAddTraceHeader");
25944
- function cloneHeaders(headers) {
25945
- return new Headers(headers);
25946
- }
25947
- __name(cloneHeaders, "cloneHeaders");
26350
+ __name(createTunnelStartupError, "createTunnelStartupError");
25948
26351
 
25949
26352
  // src/update-check.ts
25950
26353
  var import_update_check = __toESM(require_update_check());
@@ -26277,4 +26680,4 @@ safe-buffer/index.js:
26277
26680
  (*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> *)
26278
26681
  */
26279
26682
 
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 };
26683
+ 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 };