chore: import upstream snapshot with attribution
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:11 +08:00
commit 70bf21e064
5213 changed files with 731895 additions and 0 deletions
@@ -0,0 +1,18 @@
export const REDIRECTS_VERSION = 1;
export const HEADERS_VERSION = 2;
export const PERMITTED_STATUS_CODES = new Set([200, 301, 302, 303, 307, 308]);
export const HEADER_SEPARATOR = ":";
export const MAX_LINE_LENGTH = 2000;
export const MAX_HEADER_RULES = 100;
export const MAX_DYNAMIC_REDIRECT_RULES = 100;
export const MAX_STATIC_REDIRECT_RULES = 2000;
export const UNSET_OPERATOR = "! ";
export const SPLAT_REGEX = /\*/g;
export const PLACEHOLDER_REGEX = /:[A-Za-z]\w*/g;
/** Max number of rules in `run_worker_first` */
export const MAX_ROUTES_RULES = 100;
/** Max char length of each rule in `run_worker_first` */
export const MAX_ROUTES_RULE_LENGTH = 100;
@@ -0,0 +1,164 @@
import { relative } from "node:path";
import {
HEADERS_VERSION,
PLACEHOLDER_REGEX,
REDIRECTS_VERSION,
SPLAT_REGEX,
} from "./constants";
import type {
AssetConfig,
MetadataHeaders,
MetadataRedirects,
MetadataStaticRedirects,
} from "../types";
import type { Logger, ParsedHeaders, ParsedRedirects } from "./types";
export function constructRedirects({
redirects,
redirectsFile,
logger,
}: {
redirects?: ParsedRedirects;
redirectsFile?: string;
logger: Logger;
}): Pick<AssetConfig, "redirects"> {
if (!redirects) {
return {};
}
const num_valid = redirects.rules.length;
const num_invalid = redirects.invalid.length;
// exhaustive check, since we could not have parsed `redirects` out of
// a non-existing redirects file
const redirectsRelativePath = redirectsFile
? relative(process.cwd(), redirectsFile)
: "";
logger.log(
`✨ Parsed ${num_valid} valid redirect rule${num_valid === 1 ? "" : "s"}.`
);
if (num_invalid > 0) {
let invalidRedirectRulesList = ``;
for (const { line, lineNumber, message } of redirects.invalid) {
invalidRedirectRulesList += `▶︎ ${message}\n`;
if (line) {
invalidRedirectRulesList += ` at ${redirectsRelativePath}${lineNumber ? `:${lineNumber}` : ""} | ${line}\n\n`;
}
}
logger.warn(
`Found ${num_invalid} invalid redirect rule${num_invalid === 1 ? "" : "s"}:\n` +
`${invalidRedirectRulesList}`
);
}
/* Better to return no Redirects object at all than one with empty rules */
if (num_valid === 0) {
return {};
}
const staticRedirects: MetadataStaticRedirects = {};
const dynamicRedirects: MetadataRedirects = {};
let canCreateStaticRule = true;
for (const rule of redirects.rules) {
if (!rule.from.match(SPLAT_REGEX) && !rule.from.match(PLACEHOLDER_REGEX)) {
if (canCreateStaticRule) {
staticRedirects[rule.from] = {
status: rule.status,
to: rule.to,
lineNumber: rule.lineNumber,
};
continue;
} else {
logger.info(
`The redirect rule ${rule.from}${rule.status} ${rule.to} could be made more performant by bringing it above any lines with splats or placeholders.`
);
}
}
dynamicRedirects[rule.from] = { status: rule.status, to: rule.to };
canCreateStaticRule = false;
}
return {
redirects: {
version: REDIRECTS_VERSION,
staticRules: staticRedirects,
rules: dynamicRedirects,
},
};
}
export function constructHeaders({
headers,
headersFile,
logger,
}: {
headers?: ParsedHeaders;
headersFile?: string;
logger: Logger;
}): Pick<AssetConfig, "headers"> {
if (!headers) {
return {};
}
const num_valid = headers.rules.length;
const num_invalid = headers.invalid.length;
// exhaustive check, since we could not have parsed `headers` out of
// a non-existing headers file
const headersRelativePath = headersFile
? relative(process.cwd(), headersFile)
: "";
logger.log(
`✨ Parsed ${num_valid} valid header rule${num_valid === 1 ? "" : "s"}.`
);
if (num_invalid > 0) {
let invalidHeaderRulesList = ``;
for (const { line, lineNumber, message } of headers.invalid) {
invalidHeaderRulesList += `▶︎ ${message}\n`;
if (line) {
invalidHeaderRulesList += ` at ${headersRelativePath}${lineNumber ? `:${lineNumber}` : ""} | ${line}\n\n`;
}
}
logger.warn(
`Found ${num_invalid} invalid header rule${num_invalid === 1 ? "" : "s"}:\n` +
`${invalidHeaderRulesList}`
);
}
/* Better to return no Headers object at all than one with empty rules */
if (num_valid === 0) {
return {};
}
const rules: MetadataHeaders = {};
for (const rule of headers.rules) {
const configuredRule: MetadataHeaders[string] = {};
if (Object.keys(rule.headers).length) {
configuredRule.set = rule.headers;
}
if (rule.unsetHeaders.length) {
configuredRule.unset = rule.unsetHeaders;
}
rules[rule.path] = configuredRule;
}
return {
headers: {
version: HEADERS_VERSION,
rules,
},
};
}
@@ -0,0 +1,221 @@
import {
HEADER_SEPARATOR,
MAX_HEADER_RULES,
MAX_LINE_LENGTH,
SPLAT_REGEX,
UNSET_OPERATOR,
} from "./constants";
import { validateUrl } from "./validateURL";
import type { HeadersRule, InvalidHeadersRule, ParsedHeaders } from "./types";
// Not strictly necessary to check for all protocols-like beginnings, since _technically_ that could be a legit header (e.g. name=http, value=://I'm a value).
// But we're checking here since some people might be caught out and it'll help 99.9% of people who get it wrong.
// We do the proper validation in `validateUrl` anyway :)
const LINE_IS_PROBABLY_A_PATH = new RegExp(/^([^\s]+:\/\/|^\/)/);
export function parseHeaders(
input: string,
{
maxRules = MAX_HEADER_RULES,
maxLineLength = MAX_LINE_LENGTH,
}: { maxRules?: number; maxLineLength?: number } = {}
): ParsedHeaders {
const lines = input.split("\n");
const rules: HeadersRule[] = [];
const invalid: InvalidHeadersRule[] = [];
let rule: (HeadersRule & { line: string }) | undefined = undefined;
// When a path line is rejected (invalid URL, multiple wildcards, etc.),
// we silently skip subsequent header lines until the next path line
// rather than emitting confusing "Path should come before header" errors.
let skipUntilNextPath = false;
for (let i = 0; i < lines.length; i++) {
const line = (lines[i] || "").trim();
if (line.length === 0 || line.startsWith("#")) {
continue;
}
if (line.length > maxLineLength) {
invalid.push({
message: `Ignoring line ${
i + 1
} as it exceeds the maximum allowed length of ${maxLineLength}.`,
});
continue;
}
if (LINE_IS_PROBABLY_A_PATH.test(line)) {
skipUntilNextPath = false;
if (rules.length >= maxRules) {
invalid.push({
message: `Maximum number of rules supported is ${maxRules}. Skipping remaining ${
lines.length - i
} lines of file.`,
});
break;
}
if (rule) {
if (isValidRule(rule)) {
rules.push({
path: rule.path,
headers: rule.headers,
unsetHeaders: rule.unsetHeaders,
});
} else {
invalid.push({
line: rule.line,
lineNumber: i + 1,
message: "No headers specified",
});
}
}
const [path, pathError] = validateUrl(line, false, true);
if (pathError) {
invalid.push({
line,
lineNumber: i + 1,
message: pathError,
});
rule = undefined;
skipUntilNextPath = true;
continue;
}
const wildcardError = validateNoMultipleWildcards(path as string);
if (wildcardError) {
invalid.push({
line,
lineNumber: i + 1,
message: wildcardError,
});
rule = undefined;
skipUntilNextPath = true;
continue;
}
rule = {
path: path as string,
line,
headers: {},
unsetHeaders: [],
};
continue;
}
if (skipUntilNextPath) {
continue;
}
if (!line.includes(HEADER_SEPARATOR)) {
if (!rule) {
invalid.push({
line,
lineNumber: i + 1,
message: "Expected a path beginning with at least one forward-slash",
});
} else {
if (line.trim().startsWith(UNSET_OPERATOR)) {
rule.unsetHeaders.push(line.trim().replace(UNSET_OPERATOR, ""));
} else {
invalid.push({
line,
lineNumber: i + 1,
message:
"Expected a colon-separated header pair (e.g. name: value)",
});
}
}
continue;
}
const [rawName, ...rawValue] = line.split(HEADER_SEPARATOR);
const name = (rawName || "").trim().toLowerCase();
if (name.includes(" ")) {
invalid.push({
line,
lineNumber: i + 1,
message: "Header name cannot include spaces",
});
continue;
}
const value = rawValue.join(HEADER_SEPARATOR).trim();
if (name === "") {
invalid.push({
line,
lineNumber: i + 1,
message: "No header name specified",
});
continue;
}
if (value === "") {
invalid.push({
line,
lineNumber: i + 1,
message: "No header value specified",
});
continue;
}
if (!rule) {
invalid.push({
line,
lineNumber: i + 1,
message: `Path should come before header (${name}: ${value})`,
});
continue;
}
const existingValues = rule.headers[name];
rule.headers[name] = existingValues ? `${existingValues}, ${value}` : value;
}
if (rule) {
if (isValidRule(rule)) {
rules.push({
path: rule.path,
headers: rule.headers,
unsetHeaders: rule.unsetHeaders,
});
} else {
invalid.push({ line: rule.line, message: "No headers specified" });
}
}
return {
rules,
invalid,
};
}
function isValidRule(rule: HeadersRule) {
return Object.keys(rule.headers).length > 0 || rule.unsetHeaders.length > 0;
}
/**
* At runtime, `*` wildcards are converted to `:splat` placeholders. This means
* a path with multiple wildcards, or a wildcard combined with an explicit
* `:splat` placeholder, would result in duplicate `:splat` parameters which is
* unsupported.
*/
function validateNoMultipleWildcards(path: string): string | undefined {
const wildcardCount = (path.match(SPLAT_REGEX) ?? []).length;
const hasSplatPlaceholder = /:splat(?!\w)/.test(path);
if (wildcardCount > 1) {
return `Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping ${path}.`;
}
if (wildcardCount > 0 && hasSplatPlaceholder) {
return `Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping ${path}.`;
}
return undefined;
}
@@ -0,0 +1,182 @@
import {
MAX_DYNAMIC_REDIRECT_RULES,
MAX_LINE_LENGTH,
MAX_STATIC_REDIRECT_RULES,
PERMITTED_STATUS_CODES,
PLACEHOLDER_REGEX,
SPLAT_REGEX,
} from "./constants";
import { urlHasHost, validateUrl } from "./validateURL";
import type { AssetConfig } from "../types";
import type {
InvalidRedirectRule,
ParsedRedirects,
RedirectLine,
RedirectRule,
} from "./types";
export function parseRedirects(
input: string,
{
htmlHandling = undefined,
maxStaticRules = MAX_STATIC_REDIRECT_RULES,
maxDynamicRules = MAX_DYNAMIC_REDIRECT_RULES,
maxLineLength = MAX_LINE_LENGTH,
}: {
htmlHandling?: AssetConfig["html_handling"];
maxStaticRules?: number;
maxDynamicRules?: number;
maxLineLength?: number;
} = {}
): ParsedRedirects {
const lines = input.split("\n");
const rules: RedirectRule[] = [];
const seen_paths = new Set<string>();
const invalid: InvalidRedirectRule[] = [];
let staticRules = 0;
let dynamicRules = 0;
let canCreateStaticRule = true;
for (let i = 0; i < lines.length; i++) {
const line = (lines[i] || "").trim();
if (line.length === 0 || line.startsWith("#")) {
continue;
}
if (line.length > maxLineLength) {
invalid.push({
message: `Ignoring line ${
i + 1
} as it exceeds the maximum allowed length of ${maxLineLength}.`,
});
continue;
}
// Handle inline comments: strip off from the first `#` token that starts a comment, but not URL fragments.
// This allows `/a /b#fragment` but strips `/a /b # comment`
const tokens = line.replace(/\s+#.*$/, "").split(/\s+/);
if (tokens.length < 2 || tokens.length > 3) {
invalid.push({
line,
lineNumber: i + 1,
message: `Expected exactly 2 or 3 whitespace-separated tokens. Got ${tokens.length}.`,
});
continue;
}
const [str_from, str_to, str_status = "302"] = tokens as RedirectLine;
const fromResult = validateUrl(str_from, true, true, false, false);
if (fromResult[0] === undefined) {
invalid.push({
line,
lineNumber: i + 1,
message: fromResult[1],
});
continue;
}
const from = fromResult[0];
if (
canCreateStaticRule &&
!from.match(SPLAT_REGEX) &&
!from.match(PLACEHOLDER_REGEX)
) {
staticRules += 1;
if (staticRules > maxStaticRules) {
invalid.push({
message: `Maximum number of static rules supported is ${maxStaticRules}. Skipping line.`,
});
continue;
}
} else {
dynamicRules += 1;
canCreateStaticRule = false;
if (dynamicRules > maxDynamicRules) {
invalid.push({
message: `Maximum number of dynamic rules supported is ${maxDynamicRules}. Skipping remaining ${
lines.length - i
} lines of file.`,
});
break;
}
}
const toResult = validateUrl(str_to, false, false, true, true);
if (toResult[0] === undefined) {
invalid.push({
line,
lineNumber: i + 1,
message: toResult[1],
});
continue;
}
const to = toResult[0];
const status = Number(str_status);
if (isNaN(status) || !PERMITTED_STATUS_CODES.has(status)) {
invalid.push({
line,
lineNumber: i + 1,
message: `Valid status codes are 200, 301, 302 (default), 303, 307, or 308. Got ${str_status}.`,
});
continue;
}
// Here we reject two patterns:
// 1. `/* /index.html` Is always rejected.
// 2. `/ /index` Is rejected when HTML handling is enabled.
// Allowing the redirect in other cases will cause TOO_MANY_REDIRECTS errors as the asset Worker will
// redirect it back to `/` by removing the `/index.html`.
// We only want to run this on relative URLs.
const hasRelativePath = !urlHasHost(to);
const hasWildcardToIndex =
from.endsWith("/*") && /\/index(.html)?$/.test(to);
const hasRootToIndex = from.endsWith("/") && /\/index(.html)?$/.test(to);
const hasHTMLHandling = htmlHandling !== "none"; // HTML handling is enabled by default.
if (
hasRelativePath &&
(hasWildcardToIndex || (hasRootToIndex && hasHTMLHandling))
) {
invalid.push({
line,
lineNumber: i + 1,
message:
"Infinite loop detected in this rule and has been ignored. This will cause a redirect to strip `.html` or `/index` and end up triggering this rule again. Please fix or remove this rule to silence this warning.",
});
continue;
}
if (seen_paths.has(from)) {
invalid.push({
line,
lineNumber: i + 1,
message: `Ignoring duplicate rule for path ${from}.`,
});
continue;
}
seen_paths.add(from);
if (status === 200) {
if (urlHasHost(to)) {
invalid.push({
line,
lineNumber: i + 1,
message: `Proxy (200) redirects can only point to relative paths. Got ${to}`,
});
continue;
}
}
rules.push({ from, to, status, lineNumber: i + 1 });
}
return {
rules,
invalid,
};
}
@@ -0,0 +1,90 @@
import { MAX_ROUTES_RULE_LENGTH, MAX_ROUTES_RULES } from "./constants";
import type { StaticRouting } from "../types";
// copy of what EWC does. Wrangler uploads the rules in one array (so the API is consistent with Wrangler config),
// but router Worker expects the rules to be split into two arrays, which we do here.
// This logic is translated from assets/staticrouting.go.
export function parseStaticRouting(input: string[]): StaticRouting {
if (input.length === 0) {
throw new Error(
"No `run_worker_first` rules were provided; must provide at least 1 rule."
);
}
if (input.length > MAX_ROUTES_RULES) {
throw new Error(
`Too many \`run_worker_first\` rules were provided; ${input.length} rules provided exceeds max of ${MAX_ROUTES_RULES}.`
);
}
const rawAssetWorkerRules = [];
const assetWorkerRules = [];
const userWorkerRules = [];
const invalidRules = [];
for (const rule of input) {
if (rule.startsWith("!/")) {
assetWorkerRules.push(rule.slice(1)); // Remove leading !
rawAssetWorkerRules.push(rule);
} else if (rule.startsWith("/")) {
userWorkerRules.push(rule);
} else if (rule.startsWith("!")) {
invalidRules.push(`'${rule}': negative rules must start with '!/'`);
} else {
invalidRules.push(`'${rule}': rules must start with '/' or '!/'`);
}
}
if (assetWorkerRules.length > 0 && userWorkerRules.length === 0) {
throw new Error(
"Only negative `run_worker_first` rules were provided; must provide at least 1 non-negative rule"
);
}
const invalidAssetWorkerRules =
validateStaticRoutingRules(rawAssetWorkerRules);
const invalidUserWorkerRules = validateStaticRoutingRules(userWorkerRules);
const errorMessage = formatInvalidRoutes([
...invalidRules,
...invalidUserWorkerRules,
...invalidAssetWorkerRules,
]);
if (errorMessage) {
throw new Error(errorMessage);
}
return { asset_worker: assetWorkerRules, user_worker: userWorkerRules };
}
function validateStaticRoutingRules(rules: string[]): string[] {
const invalid: string[] = [];
const seen = new Set<string>();
for (const rule of rules) {
if (rule.length > MAX_ROUTES_RULE_LENGTH) {
invalid.push(
`'${rule}': all rules must be less than ${MAX_ROUTES_RULE_LENGTH} characters in length`
);
}
if (seen.has(rule)) {
invalid.push(`'${rule}': rule is a duplicate; rules must be unique`);
}
if (rule.endsWith("*")) {
// Check for redundant rules due to a glob
for (const otherRule of rules) {
if (otherRule !== rule && otherRule.startsWith(rule.slice(0, -1))) {
invalid.push(`'${otherRule}': rule '${rule}' makes it redundant`);
}
}
}
seen.add(rule);
}
return invalid;
}
const formatInvalidRoutes = (invalidRules: string[]) => {
if (invalidRules.length === 0) {
return undefined;
}
return `Invalid routes in \`run_worker_first\`:\n` + invalidRules.join("\n");
};
@@ -0,0 +1,44 @@
export type RedirectLine = [from: string, to: string, status?: number];
export type RedirectRule = {
from: string;
to: string;
status: number;
lineNumber: number;
};
export type Headers = Record<string, string>;
export type HeadersRule = {
path: string;
headers: Headers;
unsetHeaders: string[];
};
export type InvalidRedirectRule = {
line?: string;
lineNumber?: number;
message: string;
};
export type InvalidHeadersRule = {
line?: string;
lineNumber?: number;
message: string;
};
export type ParsedRedirects = {
invalid: InvalidRedirectRule[];
rules: RedirectRule[];
};
export type ParsedHeaders = {
invalid: InvalidHeadersRule[];
rules: HeadersRule[];
};
export interface Logger {
debug: (message: string) => void;
log: (message: string) => void;
info: (message: string) => void;
warn: (message: string) => void;
error: (error: Error) => void;
}
@@ -0,0 +1,76 @@
export const extractPathname = (
path = "/",
includeSearch: boolean,
includeHash: boolean
): string => {
if (!path.startsWith("/")) {
path = `/${path}`;
}
const url = new URL(`//${path}`, "relative://");
return `${url.pathname}${includeSearch ? url.search : ""}${
includeHash ? url.hash : ""
}`;
};
const URL_REGEX = /^https:\/\/+(?<host>[^/]+)\/?(?<path>.*)/;
const HOST_WITH_PORT_REGEX = /.*:\d+$/;
const PATH_REGEX = /^\//;
export const validateUrl = (
token: string,
onlyRelative = false,
disallowPorts = false,
includeSearch = false,
includeHash = false
): [undefined, string] | [string, undefined] => {
const host = URL_REGEX.exec(token);
if (host && host.groups && host.groups.host) {
if (onlyRelative) {
return [
undefined,
`Only relative URLs are allowed. Skipping absolute URL ${token}.`,
];
}
if (disallowPorts && host.groups.host.match(HOST_WITH_PORT_REGEX)) {
return [
undefined,
`Specifying ports is not supported. Skipping absolute URL ${token}.`,
];
}
return [
`https://${host.groups.host}${extractPathname(
host.groups.path,
includeSearch,
includeHash
)}`,
undefined,
];
} else {
if (!token.startsWith("/") && onlyRelative) {
token = `/${token}`;
}
const path = PATH_REGEX.exec(token);
if (path) {
try {
return [extractPathname(token, includeSearch, includeHash), undefined];
} catch {
return [undefined, `Error parsing URL segment ${token}. Skipping.`];
}
}
}
return [
undefined,
onlyRelative
? "URLs should begin with a forward-slash."
: 'URLs should either be relative (e.g. begin with a forward-slash), or use HTTPS (e.g. begin with "https://").',
];
};
export function urlHasHost(token: string): boolean {
const host = URL_REGEX.exec(token);
return Boolean(host && host.groups && host.groups.host);
}
@@ -0,0 +1,36 @@
// -- Constants for manifest encoding/decoding --
// (header and tails are not currently being used for anything afaik)
// NB these all refer to bytes
/** Reserved header at the start of the whole manifest, NOT in each entry (currently unused)
* manifest = [HEADER, [ entry = PATH_HASH, CONTENT_HASH, TAIL], [entry], ... , [entry] ]
*/
export const HEADER_SIZE = 20;
/** manifest = [HEADER, [ entry = PATH_HASH, CONTENT_HASH, TAIL], [entry], ... , [entry] ] */
export const PATH_HASH_SIZE = 16;
/** manifest = [HEADER, [ entry = PATH_HASH, CONTENT_HASH, TAIL], [entry], ... , [entry] ] */
export const CONTENT_HASH_SIZE = 16;
/** manifest = [HEADER, [ entry = PATH_HASH, CONTENT_HASH, TAIL], [entry], ... , [entry] ] */
export const TAIL_SIZE = 8;
/** offset of PATH_HASH from start of each entry
* manifest = [HEADER, [ entry = PATH_HASH, CONTENT_HASH, TAIL], [entry], ... , [entry] ] */
export const PATH_HASH_OFFSET = 0;
/** offset of CONTENT_HASH from start of each entry
* manifest = [HEADER, [ entry = PATH_HASH, CONTENT_HASH, TAIL], [entry], ... , [entry] ] */
export const CONTENT_HASH_OFFSET = PATH_HASH_SIZE;
/** manifest = [HEADER, [ entry = PATH_HASH, CONTENT_HASH, TAIL], [entry], ... , [entry] ] */
export const ENTRY_SIZE = PATH_HASH_SIZE + CONTENT_HASH_SIZE + TAIL_SIZE;
// -- Manifest creation constants --
// used in wrangler dev and deploy
/**
* Maximum number of assets that can be deployed with a Worker; this is a global
* ceiling, and may vary by the user's subscription.
*/
export const MAX_ASSET_COUNT = 100_000;
/** Maximum size per asset that can be deployed with a Worker */
export const MAX_ASSET_SIZE = 25 * 1024 * 1024;
export const CF_ASSETS_IGNORE_FILENAME = ".assetsignore";
export const REDIRECTS_FILENAME = "_redirects";
export const HEADERS_FILENAME = "_headers";
+93
View File
@@ -0,0 +1,93 @@
import { readFileSync } from "node:fs";
import { isAbsolute, resolve, sep } from "node:path";
import ignore from "ignore";
import mime from "mime";
import {
CF_ASSETS_IGNORE_FILENAME,
HEADERS_FILENAME,
REDIRECTS_FILENAME,
} from "./constants";
import type { PathOrFileDescriptor } from "node:fs";
/** normalises sep for windows and prefix with `/` */
export const normalizeFilePath = (relativeFilepath: string) => {
if (isAbsolute(relativeFilepath)) {
throw new Error(`Expected relative path`);
}
return "/" + relativeFilepath.split(sep).join("/");
};
export const getContentType = (absFilePath: string) => {
let contentType = mime.getType(absFilePath);
if (
contentType &&
contentType.startsWith("text/") &&
!contentType.includes("charset")
) {
contentType = `${contentType}; charset=utf-8`;
}
return contentType;
};
/**
* Generate a function that can match relative filepaths against a list of gitignore formatted patterns.
*/
export function createPatternMatcher(
patterns: string[],
exclude: boolean
): (filePath: string) => boolean {
if (patterns.length === 0) {
return (_filePath) => !exclude;
} else {
const ignorer = ignore().add(patterns);
return (filePath) => ignorer.test(filePath).ignored;
}
}
export function thrownIsDoesNotExistError(
thrown: unknown
): thrown is Error & { code: "ENOENT" } {
return (
thrown instanceof Error && "code" in thrown && thrown.code === "ENOENT"
);
}
export function maybeGetFile(filePath: PathOrFileDescriptor) {
try {
return readFileSync(filePath, "utf8");
} catch (e: unknown) {
if (!thrownIsDoesNotExistError(e)) {
throw e;
}
}
}
/**
* Create a function for filtering out ignored assets.
*
* The generated function takes an asset path, relative to the asset directory,
* and returns true if the asset should not be ignored.
*/
export async function createAssetsIgnoreFunction(dir: string) {
const cfAssetIgnorePath = resolve(dir, CF_ASSETS_IGNORE_FILENAME);
const ignorePatterns = [
// Ignore the `.assetsignore` file and other metafiles by default.
// The ignore lib expects unix-style paths for its patterns
`/${CF_ASSETS_IGNORE_FILENAME}`,
`/${REDIRECTS_FILENAME}`,
`/${HEADERS_FILENAME}`,
];
let assetsIgnoreFilePresent = false;
const assetsIgnore = maybeGetFile(cfAssetIgnorePath);
if (assetsIgnore !== undefined) {
assetsIgnoreFilePresent = true;
ignorePatterns.push(...assetsIgnore.split("\n"));
}
return {
assetsIgnoreFunction: createPatternMatcher(ignorePatterns, true),
assetsIgnoreFilePresent,
};
}
@@ -0,0 +1,16 @@
import type { UnsafePerformanceTimer } from "./types";
export class PerformanceTimer {
private performanceTimer;
constructor(performanceTimer?: UnsafePerformanceTimer) {
this.performanceTimer = performanceTimer;
}
now() {
if (this.performanceTimer) {
return this.performanceTimer.timeOrigin + this.performanceTimer.now();
}
return Date.now();
}
}
+144
View File
@@ -0,0 +1,144 @@
export class OkResponse extends Response {
static readonly status = 200;
constructor(body: BodyInit | null, init?: ResponseInit) {
super(body, {
...init,
status: OkResponse.status,
});
}
}
export class NotFoundResponse extends Response {
static readonly status = 404;
constructor(...[body, init]: ConstructorParameters<typeof Response>) {
super(body, {
...init,
status: NotFoundResponse.status,
statusText: "Not Found",
});
}
}
// A magical response type which is used to signal that a user worker should be invoked if one is present.
export class NoIntentResponse extends NotFoundResponse {
constructor() {
super();
}
}
export class MethodNotAllowedResponse extends Response {
static readonly status = 405;
constructor(...[body, init]: ConstructorParameters<typeof Response>) {
super(body, {
...init,
status: MethodNotAllowedResponse.status,
statusText: "Method Not Allowed",
});
}
}
export class InternalServerErrorResponse extends Response {
static readonly status = 500;
constructor(_: Error, init?: ResponseInit) {
super(null, {
...init,
status: InternalServerErrorResponse.status,
});
}
}
export class NotModifiedResponse extends Response {
static readonly status = 304;
constructor(...[_body, init]: ConstructorParameters<typeof Response>) {
super(null, {
...init,
status: NotModifiedResponse.status,
statusText: "Not Modified",
});
}
}
export class MovedPermanentlyResponse extends Response {
static readonly status = 301;
constructor(location: string, init?: ResponseInit) {
super(null, {
...init,
status: MovedPermanentlyResponse.status,
statusText: "Moved Permanently",
headers: {
...init?.headers,
Location: location,
},
});
}
}
export class FoundResponse extends Response {
static readonly status = 302;
constructor(location: string, init?: ResponseInit) {
super(null, {
...init,
status: FoundResponse.status,
statusText: "Found",
headers: {
...init?.headers,
Location: location,
},
});
}
}
export class SeeOtherResponse extends Response {
static readonly status = 303;
constructor(location: string, init?: ResponseInit) {
super(null, {
...init,
status: SeeOtherResponse.status,
statusText: "See Other",
headers: {
...init?.headers,
Location: location,
},
});
}
}
export class TemporaryRedirectResponse extends Response {
static readonly status = 307;
constructor(location: string, init?: ResponseInit) {
super(null, {
...init,
status: TemporaryRedirectResponse.status,
statusText: "Temporary Redirect",
headers: {
...init?.headers,
Location: location,
},
});
}
}
export class PermanentRedirectResponse extends Response {
static readonly status = 308;
constructor(location: string, init?: ResponseInit) {
super(null, {
...init,
status: PermanentRedirectResponse.status,
statusText: "Permanent Redirect",
headers: {
...init?.headers,
Location: location,
},
});
}
}
+68
View File
@@ -0,0 +1,68 @@
import { rewriteFramesIntegration, Toucan } from "toucan-js";
import type { ColoMetadata } from "./types";
export function setupSentry(
request: Request,
context: ExecutionContext | undefined,
dsn: string,
clientId: string,
clientSecret: string,
coloMetadata?: ColoMetadata,
versionMetadata?: WorkerVersionMetadata,
accountId?: number,
scriptId?: number
): Toucan | undefined {
// Are we running locally without access to Sentry secrets? If so, don't initialise Sentry
if (!(dsn && clientId && clientSecret)) {
return undefined;
}
const sentry = new Toucan({
dsn,
request,
context,
sampleRate: 1.0,
release: versionMetadata?.tag,
integrations: [
rewriteFramesIntegration({
iteratee(frame) {
frame.filename = "/index.js";
return frame;
},
}),
],
requestDataOptions: {
allowedHeaders: [
"user-agent",
"cf-challenge",
"accept-encoding",
"accept-language",
"cf-ray",
"content-length",
"content-type",
"host",
],
allowedSearchParams: /(.*)/,
},
transportOptions: {
headers: {
"CF-Access-Client-ID": clientId,
"CF-Access-Client-Secret": clientSecret,
},
},
});
if (coloMetadata) {
sentry.setTag("colo", coloMetadata.coloId);
sentry.setTag("metal", coloMetadata.metalId);
}
if (accountId && scriptId) {
sentry.setTag("accountId", accountId);
sentry.setTag("scriptId", scriptId);
}
sentry.setUser({ id: accountId?.toString() });
return sentry;
}
@@ -0,0 +1,63 @@
import { mkdtempSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "vitest";
import { createAssetsIgnoreFunction, getContentType } from "../helpers";
describe("assets", () => {
const tmpDir = mkdtempSync(join(tmpdir(), "wrangler-tests"));
describe(".assetsignore", () => {
it("should ignore metafiles by default", async ({ expect }) => {
const { assetsIgnoreFunction } = await createAssetsIgnoreFunction(tmpDir);
expect(assetsIgnoreFunction(".assetsignore")).toBeTruthy();
expect(assetsIgnoreFunction("_redirects")).toBeTruthy();
expect(assetsIgnoreFunction("_headers")).toBeTruthy();
// don't ignore metafiles in child directories
expect(assetsIgnoreFunction(join("child", ".assetsignore"))).toBeFalsy();
expect(assetsIgnoreFunction(join("child", "_redirects"))).toBeFalsy();
expect(assetsIgnoreFunction(join("child", "_headers"))).toBeFalsy();
});
it("should allow users to force opt-in metafiles", async ({ expect }) => {
await writeFile(
join(tmpDir, "./.assetsignore"),
"!.assetsignore\n!_redirects\n!_headers"
);
const { assetsIgnoreFunction } = await createAssetsIgnoreFunction(tmpDir);
expect(assetsIgnoreFunction(".assetsignore")).toBeFalsy();
expect(assetsIgnoreFunction("_redirects")).toBeFalsy();
expect(assetsIgnoreFunction("_headers")).toBeFalsy();
});
it("should allow users to ignore files", async ({ expect }) => {
await writeFile(
join(tmpDir, "./.assetsignore"),
"logo.png\nchild/**/*.svg\n!child/nope.svg\n/*.js"
);
const { assetsIgnoreFunction } = await createAssetsIgnoreFunction(tmpDir);
expect(assetsIgnoreFunction("abc")).toBeFalsy();
expect(assetsIgnoreFunction("logo.png")).toBeTruthy();
expect(assetsIgnoreFunction(join("child", "logo.png"))).toBeTruthy();
expect(assetsIgnoreFunction("foo.js")).toBeTruthy();
expect(assetsIgnoreFunction(join("child", "foo.js"))).toBeFalsy();
expect(assetsIgnoreFunction(join("child", "yup.svg"))).toBeTruthy();
expect(
assetsIgnoreFunction(join("child", "a", "b", "c", "yup.svg"))
).toBeTruthy();
expect(assetsIgnoreFunction(join("child", "nope.svg"))).toBeFalsy();
});
});
});
describe("getContentType", () => {
it("should return 'text/javascript", ({ expect }) => {
const contentType = getContentType("/_astro/sponsors.CIiPz7eJ.js");
expect(contentType).toBe("text/javascript; charset=utf-8");
});
});
@@ -0,0 +1,304 @@
import { test } from "vitest";
import { parseHeaders } from "../configuration/parseHeaders";
test("parseHeaders should reject malformed initial lines", ({ expect }) => {
const input = `
# A single token before a path
c
# A header before a path
Access-Control-Allow-Origin: *
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [],
invalid: [
{
line: "c",
lineNumber: 3,
message: "Expected a path beginning with at least one forward-slash",
},
{
line: "Access-Control-Allow-Origin: *",
lineNumber: 5,
message:
"Path should come before header (access-control-allow-origin: *)",
},
],
});
});
test("parseHeaders should reject invalid headers", ({ expect }) => {
const input = `
# Valid header sails through
/a
Name: Value
#
/b
I'm invalid!
!x-content-type
But: I'm okay!
! Content-Type: application/json
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [
{ path: "/a", headers: { name: "Value" }, unsetHeaders: [] },
{ path: "/b", headers: { but: "I'm okay!" }, unsetHeaders: [] },
],
invalid: [
{
line: `I'm invalid!`,
lineNumber: 7,
message: "Expected a colon-separated header pair (e.g. name: value)",
},
{
line: "!x-content-type",
lineNumber: 8,
message: "Expected a colon-separated header pair (e.g. name: value)",
},
{
line: `! Content-Type: application/json`,
lineNumber: 10,
message: "Header name cannot include spaces",
},
],
});
});
test("parseHeaders should reject lines longer than 2000 chars", ({
expect,
}) => {
const huge_line = `${Array(1001).fill("a").join("")}: ${Array(1001)
.fill("b")
.join("")}`;
const input = `
# Valid entry
/a
Name: Value
# Jumbo comment line OK, ignored as normal
${Array(1001).fill("#").join("")}
# Huge path names rejected
/b
Name: Value
${huge_line}
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [
{ path: "/a", headers: { name: "Value" }, unsetHeaders: [] },
{ path: "/b", headers: { name: "Value" }, unsetHeaders: [] },
],
invalid: [
{
message: `Ignoring line 10 as it exceeds the maximum allowed length of 2000.`,
},
],
});
});
test("parseHeaders should reject any rules after the first 100", ({
expect,
}) => {
const input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(150)
.fill(undefined)
.map((_, i) => `/a/${i}\nx-index: ${i}`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
expect(parseHeaders(input)).toEqual({
rules: Array(101)
.fill(undefined)
.map((_, i) => ({
path: "/a/" + i,
headers: { "x-index": `${i}` },
unsetHeaders: [],
})),
invalid: [
{
message: `Maximum number of rules supported is 100. Skipping remaining 100 lines of file.`,
},
],
});
});
test("parseHeaders should reject paths with multiple wildcards", ({
expect,
}) => {
const input = `
# Multiple wildcards in an absolute URL
https://*.pages.dev/*
x-custom: value
# Multiple wildcards in a relative path
/blog/*/posts/*
x-custom: value
# Single wildcard is fine
https://*.pages.dev/
x-custom: value
/blog/*
x-custom: value
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [
{
path: "https://*.pages.dev/",
headers: { "x-custom": "value" },
unsetHeaders: [],
},
{
path: "/blog/*",
headers: { "x-custom": "value" },
unsetHeaders: [],
},
],
invalid: [
{
line: "https://*.pages.dev/*",
lineNumber: 3,
message:
"Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping https://*.pages.dev/*.",
},
{
line: "/blog/*/posts/*",
lineNumber: 6,
message:
"Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping /blog/*/posts/*.",
},
],
});
});
test("parseHeaders should reject paths combining wildcard with :splat placeholder", ({
expect,
}) => {
const input = `
# Wildcard + :splat in an absolute URL
https://*.pages.dev/:splat
x-custom: value
# Wildcard + :splat in a relative path
/blog/*/:splat
x-custom: value
# Just :splat without wildcard is fine
/blog/:splat
x-custom: value
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [
{
path: "/blog/:splat",
headers: { "x-custom": "value" },
unsetHeaders: [],
},
],
invalid: [
{
line: "https://*.pages.dev/:splat",
lineNumber: 3,
message:
"Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping https://*.pages.dev/:splat.",
},
{
line: "/blog/*/:splat",
lineNumber: 6,
message:
"Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping /blog/*/:splat.",
},
],
});
});
test("parseHeaders should reject malformed URLs", ({ expect }) => {
const input = `
# Spaces should be URI encoded
/some page with spaces
valid: yup
# OK with URL escaped encoding
/some%20page
valid: yup
# Unescaped URLs are handled OK by Deno, so escape & pass them through
/://so;\`me
valid: yup
/nons:/&@%+~{}ense
valid: yup
# Absolute URLs with a non-https protocol should be rejected
https://yeah.com
valid: yup
http://nah.com/blog
invalid: things
# Absolute URLs with a port should be rejected
https://nah.com:8080
invalid: also
https://nah.com:8080/blog
invalid: 2
//yeah.com/blog
valid: things
/yeah
valid: yup
/yeah.com
valid: yup
# Anything standalone is interpreted as a invalid header pair
nah.com
:
test:
`;
const result = parseHeaders(input);
expect(result).toEqual({
invalid: [
{
line: "http://nah.com/blog",
lineNumber: 16,
message:
'URLs should either be relative (e.g. begin with a forward-slash), or use HTTPS (e.g. begin with "https://").',
},
{
line: "https://nah.com:8080",
lineNumber: 19,
message:
"Specifying ports is not supported. Skipping absolute URL https://nah.com:8080.",
},
{
line: "https://nah.com:8080/blog",
lineNumber: 21,
message:
"Specifying ports is not supported. Skipping absolute URL https://nah.com:8080/blog.",
},
{
line: "nah.com",
lineNumber: 30,
message: "Expected a colon-separated header pair (e.g. name: value)",
},
{ line: ":", lineNumber: 31, message: "No header name specified" },
{ line: "test:", lineNumber: 32, message: "No header value specified" },
],
rules: [
{
path: "/some%20page%20with%20spaces",
headers: { valid: "yup" },
unsetHeaders: [],
},
{ path: "/some%20page", headers: { valid: "yup" }, unsetHeaders: [] },
{ path: "/://so;%60me", headers: { valid: "yup" }, unsetHeaders: [] },
{
path: "/nons:/&@%+~%7B%7Dense",
headers: { valid: "yup" },
unsetHeaders: [],
},
{
path: "https://yeah.com/",
headers: { valid: "yup" },
unsetHeaders: [],
},
{
path: "//yeah.com/blog",
headers: { valid: "things" },
unsetHeaders: [],
},
{ path: "/yeah", headers: { valid: "yup" }, unsetHeaders: [] },
{ path: "/yeah.com", headers: { valid: "yup" }, unsetHeaders: [] },
],
});
});
@@ -0,0 +1,178 @@
import { test } from "vitest";
import { parseHeaders } from "../configuration/parseHeaders";
test("parseHeaders should handle a single rule", ({ expect }) => {
const input = `/a
Name: Value`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [{ path: "/a", headers: { name: "Value" }, unsetHeaders: [] }],
invalid: [],
});
});
test("parseHeaders should handle headers with exclamation marks", ({
expect,
}) => {
const input = `/a
!Name: Value`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [{ path: "/a", headers: { "!name": "Value" }, unsetHeaders: [] }],
invalid: [],
});
});
test("parseHeaders should ignore blank lines", ({ expect }) => {
const input = `
/a
Name: Value
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [{ path: "/a", headers: { name: "Value" }, unsetHeaders: [] }],
invalid: [],
});
});
test("parseHeaders should trim whitespace", ({ expect }) => {
const input = `
/a
Name : Value
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [{ path: "/a", headers: { name: "Value" }, unsetHeaders: [] }],
invalid: [],
});
});
test("parseHeaders should ignore comments", ({ expect }) => {
const input = `
# This is a comment
/a
# And one here too.
Name: Value
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [{ path: "/a", headers: { name: "Value" }, unsetHeaders: [] }],
invalid: [],
});
});
test("parseHeaders should combine headers together", ({ expect }) => {
const input = `
/a
Set-Cookie: test=cookie; expires=never
Set-Cookie: another=cookie; magic!
/b
A: ABBA
B: BABA
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [
{
path: "/a",
headers: {
"set-cookie": "test=cookie; expires=never, another=cookie; magic!",
},
unsetHeaders: [],
},
{
path: "/b",
headers: { a: "ABBA", b: "BABA" },
unsetHeaders: [],
},
],
invalid: [],
});
});
test("parseHeaders should support setting hosts", ({ expect }) => {
const input = `
https://example.com
a: a
https://example.com/
b: b
https://example.com/blog
c: c
/blog
d:d
https://:subdomain.example.*/path
e:e
`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [
{ path: "https://example.com/", headers: { a: "a" }, unsetHeaders: [] },
{ path: "https://example.com/", headers: { b: "b" }, unsetHeaders: [] },
{
path: "https://example.com/blog",
headers: { c: "c" },
unsetHeaders: [],
},
{ path: "/blog", headers: { d: "d" }, unsetHeaders: [] },
{
path: "https://:subdomain.example.*/path",
headers: { e: "e" },
unsetHeaders: [],
},
],
invalid: [],
});
});
test("parseHeaders should add unset headers", ({ expect }) => {
const input = `/a
Name: Value
! Place`;
const result = parseHeaders(input);
expect(result).toEqual({
rules: [
{ path: "/a", headers: { name: "Value" }, unsetHeaders: ["Place"] },
],
invalid: [],
});
});
test("parseHeaders should support custom limits", ({ expect }) => {
const aaa = Array(1001).fill("a").join("");
const bbb = Array(1001).fill("b").join("");
const huge_line = `${aaa}: ${bbb}`;
let input = `
# Valid entry
/a
Name: Value
# Jumbo comment line OK, ignored as normal
${Array(1001).fill("#").join("")}
# Huge path names rejected
/b
Name: Value
${huge_line}
`;
let result = parseHeaders(input, { maxLineLength: 3000 });
expect(result).toEqual({
rules: [
{ path: "/a", headers: { name: "Value" }, unsetHeaders: [] },
{ path: "/b", headers: { name: "Value", [aaa]: bbb }, unsetHeaders: [] },
],
invalid: [],
});
input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(150)
.fill(undefined)
.map((_, i) => `/a/${i}\nx-index: ${i}`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
result = parseHeaders(input, { maxRules: 200 });
expect(result.rules.length).toBe(150);
expect(result.invalid.length).toBe(0);
});
@@ -0,0 +1,370 @@
import { test } from "vitest";
import { parseRedirects } from "../configuration/parseRedirects";
// Snapshot values
const maxDynamicRedirectRules = 100;
const maxLineLength = 2000;
const maxStaticRedirectRules = 2000;
test("parseRedirects should reject malformed lines", ({ expect }) => {
const input = `
# Single token
/c
# Four tokens
/d /e 302 !important
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [],
invalid: [
{
line: `/c`,
lineNumber: 3,
message: "Expected exactly 2 or 3 whitespace-separated tokens. Got 1.",
},
{
line: `/d /e 302 !important`,
lineNumber: 5,
message: "Expected exactly 2 or 3 whitespace-separated tokens. Got 4.",
},
],
});
});
test("parseRedirects should reject invalid status codes", ({ expect }) => {
const input = `
# Valid token sails through
/a /b 301
# 418 NOT OK
/c /d 418
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b", lineNumber: 3 }],
invalid: [
{
line: `/c /d 418`,
lineNumber: 5,
message:
"Valid status codes are 200, 301, 302 (default), 303, 307, or 308. Got 418.",
},
],
});
});
test(`parseRedirects should reject duplicate 'from' paths`, ({ expect }) => {
const input = `
# Valid entry
/a /b
# Nonsensical but permitted (for now)
/b /a
# Duplicate 'from'
/a /c
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{ from: "/a", status: 302, to: "/b", lineNumber: 3 },
{ from: "/b", status: 302, to: "/a", lineNumber: 5 },
],
invalid: [
{
line: `/a /c`,
lineNumber: 7,
message: `Ignoring duplicate rule for path /a.`,
},
],
});
});
test(`parseRedirects should reject lines longer than ${maxLineLength} chars`, ({
expect,
}) => {
const huge_line = `/${Array(maxLineLength).fill("a").join("")} /${Array(
maxLineLength
)
.fill("b")
.join("")} 301`;
const input = `
# Valid entry
/a /b
# Jumbo comment line OK, ignored as normal
${Array(maxLineLength + 1)
.fill("#")
.join("")}
# Huge path names rejected
${huge_line}
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 302, to: "/b", lineNumber: 3 }],
invalid: [
{
message: `Ignoring line 7 as it exceeds the maximum allowed length of ${maxLineLength}.`,
},
],
});
});
test("parseRedirects should reject any dynamic rules after the first 100", ({
expect,
}) => {
const input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(150)
.fill(undefined)
.map((_, i) => `/a/${i}/* /b/${i}/:splat`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
expect(parseRedirects(input)).toEqual({
rules: Array(100)
.fill(undefined)
.map((_, i) => ({
from: `/a/${i}/*`,
to: `/b/${i}/:splat`,
status: 302,
lineNumber: i + 3,
})),
invalid: [
{
message: `Maximum number of dynamic rules supported is 100. Skipping remaining 52 lines of file.`,
},
],
});
});
test(`parseRedirects should reject any static rules after the first ${maxStaticRedirectRules}`, ({
expect,
}) => {
const input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(maxStaticRedirectRules + 50)
.fill(undefined)
.map((_, i) => `/a/${i} /b/${i}`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
expect(parseRedirects(input)).toEqual({
rules: Array(maxStaticRedirectRules)
.fill(undefined)
.map((_, i) => ({
from: `/a/${i}`,
to: `/b/${i}`,
status: 302,
lineNumber: i + 3,
})),
invalid: Array(50)
.fill(undefined)
.map(() => ({
message: `Maximum number of static rules supported is ${maxStaticRedirectRules}. Skipping line.`,
})),
});
});
test("parseRedirects should reject a combination of lots of static and dynamic rules", ({
expect,
}) => {
const input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(maxStaticRedirectRules + 50)
.fill(undefined)
.map((_, i) => `/a/${i} /b/${i}`)
.join("\n")}
${Array(maxDynamicRedirectRules + 50)
.fill(undefined)
.map((_, i) => `/a/${i}/* /b/${i}/:splat`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
expect(parseRedirects(input)).toEqual({
rules: [
...Array(maxStaticRedirectRules)
.fill(undefined)
.map((_, i) => ({
from: `/a/${i}`,
to: `/b/${i}`,
status: 302,
lineNumber: i + 3,
})),
...Array(maxDynamicRedirectRules)
.fill(undefined)
.map((_, i) => ({
from: `/a/${i}/*`,
to: `/b/${i}/:splat`,
status: 302,
lineNumber: i + maxStaticRedirectRules + 53,
})),
],
invalid: [
...Array(50)
.fill(undefined)
.map(() => ({
message: `Maximum number of static rules supported is ${maxStaticRedirectRules}. Skipping line.`,
})),
{
message: `Maximum number of dynamic rules supported is ${maxDynamicRedirectRules}. Skipping remaining 52 lines of file.`,
},
],
});
});
test("parseRedirects should reject malformed URLs", ({ expect }) => {
const input = `
# Spaces rejected on token length
/some page /somewhere else
# OK with URL escaped encoding
/some%20page /somewhere%20else
# Unescaped URLs are handled OK by Deno, so escape & pass them through
/://so;\`me /nons:/&@%+~{}ense
# Absolute URLs aren't OK for 'from', but are fine for 'to'
https://yeah.com https://nah.com
/nah https://yeah.com
# This is actually parsed as /yeah.com, which we might want to detect but is ok for now
yeah.com https://nah.com
`;
const result = parseRedirects(input);
expect(result).toEqual({
invalid: [
{
line: `/some page /somewhere else`,
lineNumber: 3,
message: "Expected exactly 2 or 3 whitespace-separated tokens. Got 4.",
},
{
line: `https://yeah.com https://nah.com`,
lineNumber: 9,
message:
"Only relative URLs are allowed. Skipping absolute URL https://yeah.com.",
},
],
rules: [
{
from: "/some%20page",
status: 302,
to: "/somewhere%20else",
lineNumber: 5,
},
{
from: "/://so;%60me",
status: 302,
to: "/nons:/&@%+~%7B%7Dense",
lineNumber: 7,
},
{ from: "/nah", status: 302, to: "https://yeah.com/", lineNumber: 10 },
{
from: "/yeah.com",
status: 302,
to: "https://nah.com/",
lineNumber: 12,
},
],
});
});
test("parseRedirects should reject non-relative URLs for proxying (200) redirects", ({
expect,
}) => {
const input = `
/a https://example.com/b 200
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [],
invalid: [
{
line: `/a https://example.com/b 200`,
lineNumber: 2,
message:
"Proxy (200) redirects can only point to relative paths. Got https://example.com/b",
},
],
});
});
test("parseRedirects should reject wildcard patterns to index", ({
expect,
}) => {
const input = `
/* /index.html 200
/* /index 200
/ /index.html
/ /index
`;
const invalidRedirectError =
"Infinite loop detected in this rule and has been ignored. This will cause a redirect to strip `.html` or `/index` and end up triggering this rule again. Please fix or remove this rule to silence this warning.";
const result = parseRedirects(input);
expect(result).toEqual({
rules: [],
invalid: [
{
line: "/* /index.html 200",
lineNumber: 2,
message: invalidRedirectError,
},
{
line: "/* /index 200",
lineNumber: 3,
message: invalidRedirectError,
},
{
line: "/ /index.html",
lineNumber: 4,
message: invalidRedirectError,
},
{
line: "/ /index",
lineNumber: 5,
message: invalidRedirectError,
},
],
});
});
test("parseRedirects should allow root patterns to index when HTML handling disabled", ({
expect,
}) => {
// This test documents the fix for https://github.com/cloudflare/workers-sdk/issues/11824
// Exact path matches like "/ /index.html" are valid when html_handling is "none"
// because there's no automatic index.html serving, so no infinite loop occurs.
// However, wildcard patterns like "/* /index.html" should still be blocked.
const input = `
/* /index.html 200
/* /index 200
/ /index.html
/ /index
`;
const invalidRedirectError =
"Infinite loop detected in this rule and has been ignored. This will cause a redirect to strip `.html` or `/index` and end up triggering this rule again. Please fix or remove this rule to silence this warning.";
const result = parseRedirects(input, { htmlHandling: "none" });
expect(result).toEqual({
rules: [
{
from: "/",
status: 302,
to: "/index.html",
lineNumber: 4,
},
],
invalid: [
{
line: "/* /index.html 200",
lineNumber: 2,
message: invalidRedirectError,
},
{
line: "/* /index 200",
lineNumber: 3,
message: invalidRedirectError,
},
{
line: "/ /index",
lineNumber: 5,
message: "Ignoring duplicate rule for path /.",
},
],
});
});
@@ -0,0 +1,440 @@
import { test } from "vitest";
import { parseRedirects } from "../configuration/parseRedirects";
test("parseRedirects should handle a single rule", ({ expect }) => {
const input = `/a /b 301`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b", lineNumber: 1 }],
invalid: [],
});
});
test("parseRedirects should ignore blank lines", ({ expect }) => {
const input = `
/a /b 301
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b", lineNumber: 2 }],
invalid: [],
});
});
test("parseRedirects should trim whitespace", ({ expect }) => {
const input = `
/a /b 301
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b", lineNumber: 2 }],
invalid: [],
});
});
test("parseRedirects should ignore comments", ({ expect }) => {
const input = `
# This is a comment
/a /b 301
# And one here too.
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b", lineNumber: 3 }],
invalid: [],
});
});
test("parseRedirects should handle a single comment-only line", ({
expect,
}) => {
const input = `# This is just a comment`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [],
invalid: [],
});
});
test("parseRedirects should handle an indented comment-only line", ({
expect,
}) => {
const input = ` # indented comment`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [],
invalid: [],
});
});
test("parseRedirects should handle multiple consecutive comment lines", ({
expect,
}) => {
const input = `
# First comment
# Second comment
# Indented comment
/a /b 301
# Comment after rule
# Another comment
/c /d
# Final comment
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{ from: "/a", status: 301, to: "/b", lineNumber: 5 },
{ from: "/c", status: 302, to: "/d", lineNumber: 8 },
],
invalid: [],
});
});
test("parseRedirects should handle a file with only comments", ({ expect }) => {
const input = `
# This file has no redirects
# Just comments
# Some indented
# And more comments
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [],
invalid: [],
});
});
test("parseRedirects should default to 302", ({ expect }) => {
const input = `
/a /b 302
/c /d
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{ from: "/a", status: 302, to: "/b", lineNumber: 2 },
{ from: "/c", status: 302, to: "/d", lineNumber: 3 },
],
invalid: [],
});
});
test("parseRedirects should preserve querystrings on to", ({ expect }) => {
const input = `
/a /b?query=string 302
/c?this=rejected /d 301
/ext https://some.domain:1234/route?q=string#anchor
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{ from: "/a", status: 302, to: "/b?query=string", lineNumber: 2 },
{ from: "/c", status: 301, to: "/d", lineNumber: 3 },
{
from: "/ext",
status: 302,
to: "https://some.domain:1234/route?q=string#anchor",
lineNumber: 4,
},
],
invalid: [],
});
});
test("parseRedirects should preserve fragments", ({ expect }) => {
const input = `
/a /b#blah 302
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 302, to: "/b#blah", lineNumber: 2 }],
invalid: [],
});
});
test("parseRedirects should preserve fragments which contain a hash sign", ({
expect,
}) => {
const input = `
/a /b##blah-1 302
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 302, to: "/b##blah-1", lineNumber: 2 }],
invalid: [],
});
});
test("parseRedirects should preserve fragments which contain a hash sign and are full URLs", ({
expect,
}) => {
const input = `
/a https://example.com/b##blah-1 302
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{
from: "/a",
status: 302,
to: "https://example.com/b##blah-1",
lineNumber: 2,
},
],
invalid: [],
});
});
test("parseRedirects should accept 200 (proxying) redirects", ({ expect }) => {
const input = `
/a /b 200
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{
from: "/a",
status: 200,
to: "/b",
lineNumber: 2,
},
],
invalid: [],
});
});
test("parseRedirects should accept absolute URLs that end with index.html", ({
expect,
}) => {
const input = `
/foo https://bar.com/index.html 302
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{
from: "/foo",
status: 302,
to: "https://bar.com/index.html",
lineNumber: 2,
},
],
invalid: [],
});
});
test("parseRedirects should accept going to absolute URLs with ports", ({
expect,
}) => {
const input = `
/foo https://bar.com:123/index.html 302
/cat https://cat.com:12345 302
/dog https://dog.com:12345
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{
from: "/foo",
status: 302,
to: "https://bar.com:123/index.html",
lineNumber: 2,
},
{
from: "/cat",
status: 302,
to: "https://cat.com:12345/",
lineNumber: 3,
},
{
from: "/dog",
status: 302,
to: "https://dog.com:12345/",
lineNumber: 4,
},
],
invalid: [],
});
});
test("parseRedirects should accept relative URLs that don't point to .html files", ({
expect,
}) => {
const input = `
/* /foo 200
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{
from: "/*",
status: 200,
to: "/foo",
lineNumber: 2,
},
],
invalid: [],
});
});
test("parseRedirects should support inline comments", ({ expect }) => {
const input = `/a /b 301 # redirect a to b`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b", lineNumber: 1 }],
invalid: [],
});
});
test("parseRedirects should support inline comments without status code", ({
expect,
}) => {
const input = `/a /b # redirect with default status`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 302, to: "/b", lineNumber: 1 }],
invalid: [],
});
});
test("parseRedirects should support inline comments after URL fragments", ({
expect,
}) => {
const input = `/a /b#section 301 # redirect to section`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b#section", lineNumber: 1 }],
invalid: [],
});
});
test("parseRedirects should support inline comments without space after hash", ({
expect,
}) => {
const input = `/a /b 301 #no space comment`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 301, to: "/b", lineNumber: 1 }],
invalid: [],
});
});
test("parseRedirects should support multiple rules with inline comments", ({
expect,
}) => {
const input = `
/a /b 301 # first rule
/c /d # second rule with default status
# full line comment
/e /f 307 # third rule
`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{ from: "/a", status: 301, to: "/b", lineNumber: 2 },
{ from: "/c", status: 302, to: "/d", lineNumber: 3 },
{ from: "/e", status: 307, to: "/f", lineNumber: 5 },
],
invalid: [],
});
});
test("parseRedirects should support inline comments with absolute URLs containing fragments", ({
expect,
}) => {
const input = `/a https://x.com/b#c # comment`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [
{ from: "/a", status: 302, to: "https://x.com/b#c", lineNumber: 1 },
],
invalid: [],
});
});
test("parseRedirects should support empty inline comments (just hash)", ({
expect,
}) => {
const input = `/a /b #`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 302, to: "/b", lineNumber: 1 }],
invalid: [],
});
});
test("parseRedirects should support inline comments with multiple hashes in URL fragment", ({
expect,
}) => {
const input = `/a /b##anchor # comment`;
const result = parseRedirects(input);
expect(result).toEqual({
rules: [{ from: "/a", status: 302, to: "/b##anchor", lineNumber: 1 }],
invalid: [],
});
});
test("parseRedirects should support custom limits", ({ expect }) => {
const aaa = Array(1001).fill("a").join("");
const bbb = Array(1001).fill("b").join("");
const huge_line = `/${aaa} /${bbb} 301`;
let input = `
# Valid entry
/a /b
# Jumbo comment line OK, ignored as normal
${Array(1001).fill("#").join("")}
# Huge path names rejected
${huge_line}
`;
let result = parseRedirects(input, { maxLineLength: 3000 });
expect(result).toEqual({
rules: [
{ from: "/a", status: 302, to: "/b", lineNumber: 3 },
{ from: `/${aaa}`, status: 301, to: `/${bbb}`, lineNumber: 7 },
],
invalid: [],
});
input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(150)
.fill(undefined)
.map((_, i) => `/a/${i}/* /b/${i}/:splat`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
result = parseRedirects(input, { maxDynamicRules: 200 });
expect(result.rules.length).toBe(150);
expect(result.invalid.length).toBe(0);
input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(2050)
.fill(undefined)
.map((_, i) => `/a/${i} /b/${i}`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
result = parseRedirects(input, { maxStaticRules: 3000 });
expect(result.rules.length).toBe(2050);
expect(result.invalid.length).toBe(0);
input = `
# COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
${Array(2050)
.fill(undefined)
.map((_, i) => `/a/${i} /b/${i}`)
.join("\n")}
${Array(150)
.fill(undefined)
.map((_, i) => `/a/${i}/* /b/${i}/:splat`)
.join("\n")}
# BUT DO GET COUNTED AS TOTAL LINES SKIPPED
`;
result = parseRedirects(input, {
maxDynamicRules: 200,
maxStaticRules: 3000,
});
expect(result.rules.length).toBe(2200);
expect(result.invalid.length).toBe(0);
});
@@ -0,0 +1,97 @@
import { describe, it } from "vitest";
import { parseStaticRouting } from "../configuration/parseStaticRouting";
describe("parseStaticRouting", () => {
it("throws when given empty rules", ({ expect }) => {
expect(() => parseStaticRouting([])).toThrowErrorMatchingInlineSnapshot(
`[Error: No \`run_worker_first\` rules were provided; must provide at least 1 rule.]`
);
});
it("throws when given only negative rules", ({ expect }) => {
expect(() =>
parseStaticRouting(["!/assets"])
).toThrowErrorMatchingInlineSnapshot(
`[Error: Only negative \`run_worker_first\` rules were provided; must provide at least 1 non-negative rule]`
);
});
it("throws when too many rules are provided", ({ expect }) => {
const rules = Array.from({ length: 120 }, (_, i) => `/rule/${i}`);
expect(() => parseStaticRouting(rules)).toThrowErrorMatchingInlineSnapshot(
`[Error: Too many \`run_worker_first\` rules were provided; 120 rules provided exceeds max of 100.]`
);
const userWorkerRules = Array.from({ length: 60 }, (_, i) => `/rule/${i}`);
const assetRules = Array.from({ length: 60 }, (_, i) => `!/rule/${60 + i}`);
expect(() =>
parseStaticRouting([...userWorkerRules, ...assetRules])
).toThrowErrorMatchingInlineSnapshot(
`[Error: Too many \`run_worker_first\` rules were provided; 120 rules provided exceeds max of 100.]`
);
});
it("throws when a rule is too long", ({ expect }) => {
const rule = `/api/${"a".repeat(130)}`;
expect(() => parseStaticRouting([rule])).toThrowErrorMatchingInlineSnapshot(
`
[Error: Invalid routes in \`run_worker_first\`:
'/api/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': all rules must be less than 100 characters in length]
`
);
});
it("throws when rule doesn't begin with /", ({ expect }) => {
expect(() => parseStaticRouting(["api/*", "!asset"]))
.toThrowErrorMatchingInlineSnapshot(`
[Error: Invalid routes in \`run_worker_first\`:
'api/*': rules must start with '/' or '!/'
'!asset': negative rules must start with '!/']
`);
});
it("throws when given redundant rules", ({ expect }) => {
expect(() =>
parseStaticRouting([
"/api/*",
"/oauth/callback",
"/api/some/route",
"!/api/assets/*",
])
).toThrowErrorMatchingInlineSnapshot(
`
[Error: Invalid routes in \`run_worker_first\`:
'/api/some/route': rule '/api/*' makes it redundant]
`
);
});
it("throws when given duplicate routes", ({ expect }) => {
expect(() =>
parseStaticRouting([
"/api/some/route",
"/oauth/callback",
"/api/some/route",
"!/api/assets/*",
])
).toThrowErrorMatchingInlineSnapshot(
`
[Error: Invalid routes in \`run_worker_first\`:
'/api/some/route': rule is a duplicate; rules must be unique]
`
);
});
it("correctly parses valid rules", ({ expect }) => {
const parsed = parseStaticRouting([
"/api/*",
"/oauth/callback",
"!/api/assets/*",
]);
const expected = {
user_worker: ["/api/*", "/oauth/callback"],
asset_worker: ["/api/assets/*"],
};
expect(parsed).toEqual(expected);
});
});
+32
View File
@@ -0,0 +1,32 @@
import type { JaegerTracing, Span } from "./types";
export function mockJaegerBindingSpan(): Span {
return {
addLogs: () => {},
setTags: () => {},
end: () => {},
isRecording: true,
};
}
export function mockJaegerBinding(): JaegerTracing {
return {
enterSpan: (_, span, ...args) => {
return span(mockJaegerBindingSpan(), ...args);
},
getSpanContext: () => ({
traceId: "test-trace",
spanId: "test-span",
parentSpanId: "test-parent-span",
traceFlags: 0,
}),
runWithSpanContext: (_, callback, ...args) => {
return callback(...args);
},
traceId: "test-trace",
spanId: "test-span",
parentSpanId: "test-parent-span",
cfTraceIdHeader: "test-trace:test-span:0",
};
}
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es2021",
"lib": ["es2021"],
"module": "NodeNext",
"moduleResolution": "nodenext",
"types": ["@cloudflare/workers-types/experimental", "@types/node"],
"noEmit": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"skipLibCheck": true
}
}
+140
View File
@@ -0,0 +1,140 @@
import { z } from "zod";
const InternalConfigSchema = z.object({
account_id: z.number().optional(),
script_id: z.number().optional(),
debug: z.boolean().optional(),
});
const StaticRoutingSchema = z.object({
user_worker: z.array(z.string()),
asset_worker: z.array(z.string()).optional(),
});
export type StaticRouting = z.infer<typeof StaticRoutingSchema>;
export const RouterConfigSchema = z.object({
invoke_user_worker_ahead_of_assets: z.boolean().optional(),
static_routing: StaticRoutingSchema.optional(),
has_user_worker: z.boolean().optional(),
...InternalConfigSchema.shape,
});
export const EyeballRouterConfigSchema = z.union([
z.object({
limitedAssetsOnly: z.boolean().optional(),
}),
z.null(),
]);
const MetadataStaticRedirectEntry = z.object({
status: z.number(),
to: z.string(),
lineNumber: z.number(),
});
const MetadataRedirectEntry = z.object({
status: z.number(),
to: z.string(),
});
const MetadataStaticRedirects = z.record(MetadataStaticRedirectEntry);
export type MetadataStaticRedirects = z.infer<typeof MetadataStaticRedirects>;
const MetadataRedirects = z.record(MetadataRedirectEntry);
export type MetadataRedirects = z.infer<typeof MetadataRedirects>;
const MetadataHeaderEntry = z.object({
set: z.record(z.string()).optional(),
unset: z.array(z.string()).optional(),
});
const MetadataHeaders = z.record(MetadataHeaderEntry);
export type MetadataHeaders = z.infer<typeof MetadataHeaders>;
export const RedirectsSchema = z
.object({
version: z.literal(1),
staticRules: MetadataStaticRedirects,
rules: MetadataRedirects,
})
.optional();
export const HeadersSchema = z
.object({
version: z.literal(2),
rules: MetadataHeaders,
})
.optional();
export const AssetConfigSchema = z.object({
compatibility_date: z.string().optional(),
compatibility_flags: z.array(z.string()).optional(),
html_handling: z
.enum([
"auto-trailing-slash",
"force-trailing-slash",
"drop-trailing-slash",
"none",
])
.optional(),
not_found_handling: z
.enum(["single-page-application", "404-page", "none"])
.optional(),
redirects: RedirectsSchema,
headers: HeadersSchema,
has_static_routing: z.boolean().optional(),
...InternalConfigSchema.shape,
});
export type EyeballRouterConfig = z.infer<typeof EyeballRouterConfigSchema>;
export type RouterConfig = z.infer<typeof RouterConfigSchema>;
export type AssetConfig = z.infer<typeof AssetConfigSchema>;
export interface UnsafePerformanceTimer {
readonly timeOrigin: number;
now: () => number;
}
export interface JaegerTracing {
enterSpan<T extends unknown[], R = void>(
name: string,
span: (s: Span, ...args: T) => R,
...args: T
): R;
getSpanContext(): SpanContext | null;
runWithSpanContext<T extends unknown[]>(
spanContext: SpanContext | null,
callback: (...args: T) => unknown,
...args: T
): unknown;
readonly traceId: string | null;
readonly spanId: string | null;
readonly parentSpanId: string | null;
readonly cfTraceIdHeader: string | null;
}
export interface Span {
addLogs(logs: JaegerRecord): void;
setTags(tags: JaegerRecord): void;
end(): void;
isRecording: boolean;
}
export interface SpanContext {
traceId: string;
spanId: string;
parentSpanId: string;
traceFlags: number;
}
export type JaegerValue = string | number | boolean;
export type JaegerRecord = Record<string, JaegerValue>;
export interface ColoMetadata {
metalId: number;
coloId: number;
coloRegion: string;
coloTier: number;
}