@netlify/redirects 1.1.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/main.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { Handler } from '@netlify/dev-utils';
2
+
3
+ type HttpStatusCode = number;
4
+ interface Redirect {
5
+ from: string;
6
+ to?: string;
7
+ status?: HttpStatusCode;
8
+ force?: boolean;
9
+ signed?: string;
10
+ query?: Partial<Record<string, string>>;
11
+ headers?: Partial<Record<string, string>>;
12
+ conditions?: Partial<Record<'Language' | 'Role' | 'Country' | 'Cookie', readonly string[]>>;
13
+ }
14
+
15
+ interface RedirectsMatch {
16
+ error?: Error;
17
+ external: boolean;
18
+ force: boolean;
19
+ headers: Record<string, string>;
20
+ hiddenProxy: boolean;
21
+ redirect: boolean;
22
+ statusCode: number;
23
+ target: URL;
24
+ targetRelative: string;
25
+ }
26
+ interface RedirectsHandlerOptions {
27
+ configPath?: string | undefined;
28
+ configRedirects: Redirect[];
29
+ geoCountry?: string | undefined;
30
+ jwtRoleClaim: string;
31
+ jwtSecret: string;
32
+ notFoundHandler?: Handler;
33
+ projectDir: string;
34
+ publicDir?: string | undefined;
35
+ siteID?: string;
36
+ siteURL?: string;
37
+ }
38
+ type GetStaticFile = (request: Request) => Promise<(() => Promise<Response>) | undefined>;
39
+ declare class RedirectsHandler {
40
+ private notFoundHandler;
41
+ private rewriter;
42
+ private siteID;
43
+ private siteURL;
44
+ constructor({ configPath, configRedirects, geoCountry, jwtRoleClaim, jwtSecret, notFoundHandler, projectDir, publicDir, siteID, siteURL, }: RedirectsHandlerOptions);
45
+ match(request: Request): Promise<RedirectsMatch | undefined>;
46
+ handle(request: Request, match: RedirectsMatch, getStaticFile: GetStaticFile): Promise<Response | undefined>;
47
+ }
48
+
49
+ export { RedirectsHandler };
package/dist/main.js ADDED
@@ -0,0 +1,241 @@
1
+ // src/main.ts
2
+ import process from "node:process";
3
+
4
+ // src/lib/rewriter.ts
5
+ import path from "node:path";
6
+ import { toMultiValueHeaders } from "@netlify/dev-utils";
7
+ import cookie from "cookie";
8
+ import redirector from "netlify-redirector";
9
+
10
+ // src/lib/parser.ts
11
+ import { parseAllRedirects } from "@netlify/redirect-parser";
12
+ var parseRedirects = async function({
13
+ configRedirects = [],
14
+ configPath,
15
+ redirectsFiles
16
+ }) {
17
+ const parsed = await parseAllRedirects({
18
+ redirectsFiles,
19
+ netlifyConfigPath: configPath,
20
+ minimal: false,
21
+ // @ts-expect-error TODO: Fix this type in `@netlify/redirect-parser`.
22
+ configRedirects
23
+ });
24
+ handleRedirectParsingErrors(parsed.errors);
25
+ return parsed.redirects.map(normalizeRedirect);
26
+ };
27
+ var handleRedirectParsingErrors = (errors) => {
28
+ if (errors.length === 0) {
29
+ return;
30
+ }
31
+ const errorMessage = errors.map(({ message }) => message).join("\n\n");
32
+ console.log(`Redirects syntax errors:
33
+ ${errorMessage}`);
34
+ };
35
+ var normalizeRedirect = function(input) {
36
+ const { conditions, from, query, signed, ...redirect } = input;
37
+ return {
38
+ ...redirect,
39
+ origin: from,
40
+ params: query,
41
+ conditions,
42
+ ...signed && {
43
+ sign: {
44
+ jwt_secret: signed
45
+ }
46
+ }
47
+ };
48
+ };
49
+
50
+ // src/lib/rewriter.ts
51
+ var REDIRECTS_FILE_NAME = "_redirects";
52
+ var getLanguage = (acceptLanguage) => {
53
+ if (acceptLanguage) {
54
+ return acceptLanguage.split(",")[0].slice(0, 2);
55
+ }
56
+ return "en";
57
+ };
58
+ var createRewriter = async function({
59
+ configPath,
60
+ configRedirects,
61
+ geoCountry,
62
+ jwtRoleClaim,
63
+ jwtSecret,
64
+ projectDir,
65
+ publicDir
66
+ }) {
67
+ let matcher = null;
68
+ const redirectsFiles = [
69
+ .../* @__PURE__ */ new Set([path.resolve(publicDir ?? "", REDIRECTS_FILE_NAME), path.resolve(projectDir, REDIRECTS_FILE_NAME)])
70
+ ];
71
+ const redirects = await parseRedirects({ configRedirects, redirectsFiles, configPath });
72
+ const getMatcher = async () => {
73
+ if (matcher)
74
+ return matcher;
75
+ if (redirects.length !== 0) {
76
+ return matcher = await redirector.parseJSON(JSON.stringify(redirects), {
77
+ jwtSecret,
78
+ jwtRoleClaim
79
+ });
80
+ }
81
+ return {
82
+ match() {
83
+ return null;
84
+ }
85
+ };
86
+ };
87
+ return async function rewriter(req) {
88
+ const matcherFunc = await getMatcher();
89
+ const reqUrl = new URL(req.url);
90
+ const cookieValues = cookie.parse(req.headers.get("cookie") || "");
91
+ const headers = {
92
+ "x-language": cookieValues.nf_lang || getLanguage(req.headers.get("accept-language")),
93
+ "x-country": cookieValues.nf_country || geoCountry || "us",
94
+ ...toMultiValueHeaders(req.headers)
95
+ };
96
+ const matchReq = {
97
+ scheme: reqUrl.protocol.replace(/:.*$/, ""),
98
+ host: reqUrl.hostname,
99
+ path: decodeURIComponent(reqUrl.pathname),
100
+ query: reqUrl.search.slice(1),
101
+ headers,
102
+ cookieValues,
103
+ getHeader: (name) => {
104
+ const val = headers[name.toLowerCase()];
105
+ if (Array.isArray(val)) {
106
+ return val[0];
107
+ }
108
+ return val || "";
109
+ },
110
+ getCookie: (key) => cookieValues[key] || ""
111
+ };
112
+ return matcherFunc.match(matchReq);
113
+ };
114
+ };
115
+
116
+ // src/lib/signer.ts
117
+ import jwt from "jsonwebtoken";
118
+ var signRedirect = ({ deployContext, secret, siteID, siteURL }) => {
119
+ const claims = {
120
+ deploy_context: deployContext,
121
+ netlify_id: siteID,
122
+ site_url: siteURL
123
+ };
124
+ const options = {
125
+ expiresIn: "5 minutes",
126
+ issuer: "netlify"
127
+ };
128
+ return jwt.sign(claims, secret, options);
129
+ };
130
+
131
+ // src/main.ts
132
+ var RedirectsHandler = class {
133
+ notFoundHandler;
134
+ rewriter;
135
+ siteID;
136
+ siteURL;
137
+ constructor({
138
+ configPath,
139
+ configRedirects,
140
+ geoCountry,
141
+ jwtRoleClaim,
142
+ jwtSecret,
143
+ notFoundHandler,
144
+ projectDir,
145
+ publicDir,
146
+ siteID,
147
+ siteURL
148
+ }) {
149
+ this.notFoundHandler = notFoundHandler ?? (() => Promise.resolve(new Response("Not found", { status: 404 })));
150
+ this.rewriter = createRewriter({
151
+ configPath,
152
+ configRedirects,
153
+ geoCountry,
154
+ jwtRoleClaim,
155
+ jwtSecret,
156
+ projectDir,
157
+ publicDir
158
+ });
159
+ this.siteID = siteID ?? "0";
160
+ this.siteURL = siteURL ?? "http://localhost";
161
+ }
162
+ async match(request) {
163
+ const rewriter = await this.rewriter;
164
+ const rule = await rewriter(request);
165
+ if (!rule) {
166
+ return;
167
+ }
168
+ if (rule.force404) {
169
+ return {
170
+ external: false,
171
+ force: true,
172
+ headers: {},
173
+ hiddenProxy: false,
174
+ redirect: false,
175
+ statusCode: 404,
176
+ target: new URL(""),
177
+ targetRelative: ""
178
+ };
179
+ }
180
+ const requestURL = new URL(request.url);
181
+ const headers = {
182
+ ...rule.force404 ? {} : rule.proxyHeaders
183
+ };
184
+ const hiddenProxy = Object.entries(headers).some(
185
+ ([key, val]) => key.toLowerCase() === "x-nf-hidden-proxy" && val === "true"
186
+ );
187
+ const target = new URL(rule.to, request.url);
188
+ const match = {
189
+ external: "to" in rule && /^https?:\/\//.exec(rule.to) != null,
190
+ force: rule.force,
191
+ headers,
192
+ hiddenProxy,
193
+ redirect: "status" in rule && rule.status != null && rule.status >= 300 && rule.status <= 400,
194
+ statusCode: rule.status,
195
+ target,
196
+ targetRelative: `${rule.to}${requestURL.search}${requestURL.hash}`
197
+ };
198
+ if (target.searchParams.size === 0) {
199
+ requestURL.searchParams.forEach((val, key) => {
200
+ target.searchParams.append(key, val);
201
+ });
202
+ }
203
+ if (rule.signingSecret) {
204
+ const signingSecretVar = process.env[rule.signingSecret];
205
+ if (signingSecretVar) {
206
+ match.headers["x-nf-sign"] = signRedirect({
207
+ deployContext: "dev",
208
+ secret: signingSecretVar,
209
+ siteID: this.siteID,
210
+ siteURL: this.siteURL
211
+ });
212
+ } else {
213
+ match.error = new Error(`Could not sign redirect because environment variable ${rule.signingSecret} is not set`);
214
+ }
215
+ }
216
+ return match;
217
+ }
218
+ async handle(request, match, getStaticFile) {
219
+ if (match.force && match.statusCode === 404) {
220
+ return this.notFoundHandler(request);
221
+ }
222
+ const sourceStaticFile = await getStaticFile(request);
223
+ if (sourceStaticFile && !match.force) {
224
+ return sourceStaticFile();
225
+ }
226
+ if (match.redirect) {
227
+ return Response.redirect(match.target, match.statusCode);
228
+ }
229
+ if (match.external) {
230
+ const req = new Request(match.target, request);
231
+ return fetch(req);
232
+ }
233
+ const targetStaticFile = await getStaticFile(new Request(match.target, request));
234
+ if (targetStaticFile) {
235
+ return targetStaticFile();
236
+ }
237
+ }
238
+ };
239
+ export {
240
+ RedirectsHandler
241
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@netlify/redirects",
3
+ "version": "1.1.0",
4
+ "description": "JavaScript implementtion of Netlify's static file serving logic",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^14.16.0 || >=16.0.0"
8
+ },
9
+ "main": "./dist/main.cjs",
10
+ "module": "./dist/main.js",
11
+ "types": "./dist/main.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "require": {
15
+ "types": "./dist/main.d.cts",
16
+ "default": "./dist/main.cjs"
17
+ },
18
+ "import": {
19
+ "types": "./dist/main.d.ts",
20
+ "default": "./dist/main.js"
21
+ },
22
+ "default": {
23
+ "types": "./dist/main.d.ts",
24
+ "default": "./dist/main.js"
25
+ }
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "files": [
30
+ "dist/**/*",
31
+ "server.d.ts"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsup-node",
35
+ "prepack": "npm run build",
36
+ "test": "vitest run",
37
+ "test:dev": "vitest",
38
+ "test:ci": "npm run build && vitest run",
39
+ "dev": "tsup-node --watch"
40
+ },
41
+ "keywords": [],
42
+ "license": "MIT",
43
+ "repository": "netlify/primitives",
44
+ "bugs": {
45
+ "url": "https://github.com/netlify/primitives/issues"
46
+ },
47
+ "author": "Netlify Inc.",
48
+ "directories": {
49
+ "test": "test"
50
+ },
51
+ "devDependencies": {
52
+ "@netlify/dev-utils": "1.1.0",
53
+ "@types/http-proxy": "^1.17.16",
54
+ "@types/jsonwebtoken": "9.0.9"
55
+ },
56
+ "dependencies": {
57
+ "cookie": "^1.0.2",
58
+ "jsonwebtoken": "9.0.2",
59
+ "netlify-redirect-parser": "^14.4.0",
60
+ "netlify-redirector": "^0.5.0"
61
+ }
62
+ }