@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 +49 -0
- package/dist/main.js +241 -0
- package/package.json +62 -0
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
|
+
}
|