@coralreefventures/site-tools 0.1.0 → 0.2.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/README.md +56 -5
- package/dist/bin/reef-door-bundle.d.ts +3 -0
- package/dist/bin/reef-door-bundle.d.ts.map +1 -0
- package/dist/bin/reef-door-bundle.js +56 -0
- package/dist/bin/reef-door-bundle.js.map +1 -0
- package/dist/bin/reef-lighthouse.js +6 -5
- package/dist/bin/reef-lighthouse.js.map +1 -1
- package/dist/door/config.d.ts +44 -0
- package/dist/door/config.d.ts.map +1 -0
- package/dist/door/config.js +89 -0
- package/dist/door/config.js.map +1 -0
- package/dist/door/next.d.ts +12 -0
- package/dist/door/next.d.ts.map +1 -0
- package/dist/door/next.js +36 -0
- package/dist/door/next.js.map +1 -0
- package/dist/door/package.d.ts +66 -0
- package/dist/door/package.d.ts.map +1 -0
- package/dist/door/package.js +150 -0
- package/dist/door/package.js.map +1 -0
- package/dist/door/server.d.ts +48 -0
- package/dist/door/server.d.ts.map +1 -0
- package/dist/door/server.js +272 -0
- package/dist/door/server.js.map +1 -0
- package/dist/door/verify.d.ts +52 -0
- package/dist/door/verify.d.ts.map +1 -0
- package/dist/door/verify.js +133 -0
- package/dist/door/verify.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/lighthouse.d.ts +1 -1
- package/dist/lighthouse.d.ts.map +1 -1
- package/dist/lighthouse.js +1 -1
- package/dist/lighthouse.js.map +1 -1
- package/dist/serve.d.ts +26 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +84 -39
- package/dist/serve.js.map +1 -1
- package/package.json +5 -3
- package/src/bin/reef-door-bundle.ts +60 -0
- package/src/bin/reef-lighthouse.ts +6 -5
- package/src/door/config.ts +112 -0
- package/src/door/next.ts +35 -0
- package/src/door/package.ts +184 -0
- package/src/door/server.ts +308 -0
- package/src/door/verify.ts +152 -0
- package/src/index.ts +45 -1
- package/src/lighthouse.ts +2 -2
- package/src/serve.ts +101 -29
package/dist/lighthouse.js
CHANGED
|
@@ -65,7 +65,7 @@ export const runLighthouse = async (options = {}) => {
|
|
|
65
65
|
const log = options.log ?? console.log;
|
|
66
66
|
const paths = options.paths?.length ? options.paths : await pagesFromSitemap(root);
|
|
67
67
|
const chrome = options.chrome ?? (await playwrightChromium());
|
|
68
|
-
const { server, url: origin } = await serve({ root, port: options.port ??
|
|
68
|
+
const { server, url: origin } = await serve({ root, port: options.port ?? 0 });
|
|
69
69
|
const results = [];
|
|
70
70
|
try {
|
|
71
71
|
await mkdir(reportDir, { recursive: true });
|
package/dist/lighthouse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lighthouse.js","sourceRoot":"","sources":["../src/lighthouse.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"lighthouse.js","sourceRoot":"","sources":["../src/lighthouse.ts"],"names":[],"mappings":"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"}
|
package/dist/serve.d.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import { type Server } from "node:http";
|
|
1
|
+
import { type IncomingMessage, type OutgoingHttpHeaders, type Server, type ServerResponse } from "node:http";
|
|
2
|
+
/** The Content-Type a file is served with, by its extension. */
|
|
3
|
+
export declare const contentTypeOf: (file: string) => string;
|
|
2
4
|
/** Brotli or gzip for text, as the browser accepts; images and fonts are already compressed. */
|
|
3
5
|
export declare const encodingFor: (type: string, accepts: string) => "br" | "gzip" | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* What a decoded path names inside an export, the way Amplify Hosting maps one: a file, a directory's index.html
|
|
8
|
+
* when the path ends in a slash, a directory reached without its slash (the caller redirects), nothing, or a path
|
|
9
|
+
* outside the export altogether (the caller refuses it).
|
|
10
|
+
*/
|
|
11
|
+
export type StaticTarget = {
|
|
12
|
+
kind: "file";
|
|
13
|
+
file: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "directory";
|
|
16
|
+
} | {
|
|
17
|
+
kind: "missing";
|
|
18
|
+
notFound: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "outside";
|
|
21
|
+
};
|
|
22
|
+
export declare const staticTarget: (root: string, decodedPath: string) => Promise<StaticTarget>;
|
|
23
|
+
/**
|
|
24
|
+
* Streams one file, typed by its extension and compressed as the request accepts, with any extra headers the caller
|
|
25
|
+
* adds. A HEAD request gets the headers alone. A file that cannot be read after the headers are out ends the
|
|
26
|
+
* response rather than sending anything else.
|
|
27
|
+
*/
|
|
28
|
+
export declare const sendFile: (request: IncomingMessage, response: ServerResponse, file: string, status?: number, headers?: OutgoingHttpHeaders) => Promise<void>;
|
|
4
29
|
/**
|
|
5
30
|
* A server for a static export the way Amplify Hosting serves one, for screen tests and Lighthouse runs: a directory
|
|
6
31
|
* answers with its index.html, a directory path without its slash redirects to it, anything else is the 404 page
|
package/dist/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,MAAM,EACX,KAAK,cAAc,EACpB,MAAM,WAAW,CAAC;AA4BnB,gEAAgE;AAChE,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAAiE,CAAC;AAK/G,gGAAgG;AAChG,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,IAAI,GAAG,MAAM,GAAG,SAK3E,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB,eAAO,MAAM,YAAY,GAAU,MAAM,MAAM,EAAE,aAAa,MAAM,KAAG,OAAO,CAAC,YAAY,CAkB1F,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,SAAS,eAAe,EACxB,UAAU,cAAc,EACxB,MAAM,MAAM,EACZ,eAAY,EACZ,UAAS,mBAAwB,KAChC,OAAO,CAAC,IAAI,CAqBd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,MAoB9C,CAAC;AAEL,MAAM,MAAM,YAAY,GAAG;IACzB,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6BAA6B;AAC7B,eAAO,MAAM,KAAK,GAAU,kBAGzB,YAAiB,KAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAS7D,CAAC"}
|
package/dist/serve.js
CHANGED
|
@@ -1,72 +1,117 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
|
-
import { createServer } from "node:http";
|
|
3
|
+
import { createServer, } from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createBrotliCompress, createGzip } from "node:zlib";
|
|
6
|
+
// This module is also part of the door gate's compute bundle (door/package.ts copies it), so it imports nothing but
|
|
7
|
+
// Node's own modules.
|
|
6
8
|
const types = {
|
|
7
9
|
".html": "text/html; charset=utf-8",
|
|
8
10
|
".js": "text/javascript; charset=utf-8",
|
|
11
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
9
12
|
".css": "text/css; charset=utf-8",
|
|
10
13
|
".json": "application/json",
|
|
14
|
+
".webmanifest": "application/manifest+json",
|
|
11
15
|
".txt": "text/plain; charset=utf-8",
|
|
12
16
|
".xml": "application/xml",
|
|
13
17
|
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".gif": "image/gif",
|
|
21
|
+
".webp": "image/webp",
|
|
22
|
+
".avif": "image/avif",
|
|
14
23
|
".svg": "image/svg+xml",
|
|
24
|
+
".woff": "font/woff",
|
|
15
25
|
".woff2": "font/woff2",
|
|
16
26
|
".ico": "image/x-icon",
|
|
17
27
|
};
|
|
28
|
+
/** The Content-Type a file is served with, by its extension. */
|
|
29
|
+
export const contentTypeOf = (file) => types[path.extname(file)] ?? "application/octet-stream";
|
|
18
30
|
const isFile = async (file) => (await stat(file).catch(() => null))?.isFile() ?? false;
|
|
19
31
|
const isDirectory = async (file) => (await stat(file).catch(() => null))?.isDirectory() ?? false;
|
|
20
32
|
/** Brotli or gzip for text, as the browser accepts; images and fonts are already compressed. */
|
|
21
33
|
export const encodingFor = (type, accepts) => {
|
|
22
|
-
if (!/^(text\/|application\/(json|xml)|image\/svg)/.test(type)) {
|
|
34
|
+
if (!/^(text\/|application\/(json|xml|manifest\+json)|image\/svg)/.test(type)) {
|
|
23
35
|
return undefined;
|
|
24
36
|
}
|
|
25
37
|
return accepts.includes("br") ? "br" : accepts.includes("gzip") ? "gzip" : undefined;
|
|
26
38
|
};
|
|
39
|
+
export const staticTarget = async (root, decodedPath) => {
|
|
40
|
+
const base = path.resolve(root);
|
|
41
|
+
const file = path.join(base, decodedPath);
|
|
42
|
+
if (file !== base && !file.startsWith(`${base}${path.sep}`)) {
|
|
43
|
+
return { kind: "outside" };
|
|
44
|
+
}
|
|
45
|
+
const notFound = path.join(base, "404.html");
|
|
46
|
+
if (await isFile(file)) {
|
|
47
|
+
return { kind: "file", file };
|
|
48
|
+
}
|
|
49
|
+
if (await isDirectory(file)) {
|
|
50
|
+
if (decodedPath.endsWith("/")) {
|
|
51
|
+
const index = path.join(file, "index.html");
|
|
52
|
+
return (await isFile(index)) ? { kind: "file", file: index } : { kind: "missing", notFound };
|
|
53
|
+
}
|
|
54
|
+
return { kind: "directory" };
|
|
55
|
+
}
|
|
56
|
+
return { kind: "missing", notFound };
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Streams one file, typed by its extension and compressed as the request accepts, with any extra headers the caller
|
|
60
|
+
* adds. A HEAD request gets the headers alone. A file that cannot be read after the headers are out ends the
|
|
61
|
+
* response rather than sending anything else.
|
|
62
|
+
*/
|
|
63
|
+
export const sendFile = async (request, response, file, status = 200, headers = {}) => {
|
|
64
|
+
if (!(await isFile(file))) {
|
|
65
|
+
response.writeHead(status, { ...headers, "Content-Length": "0" }).end();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const type = contentTypeOf(file);
|
|
69
|
+
const encoding = encodingFor(type, String(request.headers["accept-encoding"] ?? ""));
|
|
70
|
+
response.writeHead(status, {
|
|
71
|
+
...headers,
|
|
72
|
+
"Content-Type": type,
|
|
73
|
+
Vary: "Accept-Encoding",
|
|
74
|
+
...(encoding && { "Content-Encoding": encoding }),
|
|
75
|
+
});
|
|
76
|
+
if (request.method === "HEAD") {
|
|
77
|
+
response.end();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const body = createReadStream(file);
|
|
81
|
+
const compressor = encoding === "br" ? createBrotliCompress() : encoding === "gzip" ? createGzip() : undefined;
|
|
82
|
+
body.on("error", () => response.destroy());
|
|
83
|
+
(compressor ? body.pipe(compressor) : body).pipe(response);
|
|
84
|
+
};
|
|
27
85
|
/**
|
|
28
86
|
* A server for a static export the way Amplify Hosting serves one, for screen tests and Lighthouse runs: a directory
|
|
29
87
|
* answers with its index.html, a directory path without its slash redirects to it, anything else is the 404 page
|
|
30
88
|
* with status 404, and text is compressed (Brotli or gzip) as the CDN compresses it. Not started: `listen` it, or
|
|
31
89
|
* call `serve`.
|
|
32
90
|
*/
|
|
33
|
-
export const createStaticServer = (root) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else if (await isDirectory(file)) {
|
|
58
|
-
if (url.pathname.endsWith("/")) {
|
|
59
|
-
send(path.join(file, "index.html"));
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
response.writeHead(301, { Location: `${url.pathname}/${url.search}` }).end();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
send(path.join(base, "404.html"), 404);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
};
|
|
91
|
+
export const createStaticServer = (root) => createServer(async (request, response) => {
|
|
92
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
93
|
+
let decoded;
|
|
94
|
+
try {
|
|
95
|
+
decoded = decodeURIComponent(url.pathname);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
response.writeHead(400).end();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const target = await staticTarget(root, decoded);
|
|
102
|
+
if (target.kind === "outside") {
|
|
103
|
+
response.writeHead(400).end();
|
|
104
|
+
}
|
|
105
|
+
else if (target.kind === "file") {
|
|
106
|
+
await sendFile(request, response, target.file);
|
|
107
|
+
}
|
|
108
|
+
else if (target.kind === "directory") {
|
|
109
|
+
response.writeHead(301, { Location: `${url.pathname}/${url.search}` }).end();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await sendFile(request, response, target.notFound, 404);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
70
115
|
/** The server, listening. */
|
|
71
116
|
export const serve = async ({ root = "out", port = 3140, } = {}) => {
|
|
72
117
|
const server = createStaticServer(root);
|
package/dist/serve.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EACL,YAAY,GAKb,MAAM,WAAW,CAAC;AACnB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE7D,oHAAoH;AACpH,sBAAsB;AAEtB,MAAM,KAAK,GAA2B;IACpC,OAAO,EAAE,0BAA0B;IACnC,KAAK,EAAE,gCAAgC;IACvC,MAAM,EAAE,gCAAgC;IACxC,MAAM,EAAE,yBAAyB;IACjC,OAAO,EAAE,kBAAkB;IAC3B,cAAc,EAAE,2BAA2B;IAC3C,MAAM,EAAE,2BAA2B;IACnC,MAAM,EAAE,iBAAiB;IACzB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,eAAe;IACvB,OAAO,EAAE,WAAW;IACpB,QAAQ,EAAE,YAAY;IACtB,MAAM,EAAE,cAAc;CACvB,CAAC;AAEF,gEAAgE;AAChE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,0BAA0B,CAAC;AAE/G,MAAM,MAAM,GAAG,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,KAAK,CAAC;AAC/F,MAAM,WAAW,GAAG,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,KAAK,CAAC;AAEzG,gGAAgG;AAChG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,OAAe,EAA6B,EAAE;IACtF,IAAI,CAAC,6DAA6D,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9E,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACvF,CAAC,CAAC;AAaF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,IAAY,EAAE,WAAmB,EAAyB,EAAE;IAC7F,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC1C,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QAC5D,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC7B,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC7C,IAAI,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAChC,CAAC;IACD,IAAI,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,IAAI,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;YAC5C,OAAO,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;QAC/F,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;AACvC,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAC3B,OAAwB,EACxB,QAAwB,EACxB,IAAY,EACZ,MAAM,GAAG,GAAG,EACZ,UAA+B,EAAE,EAClB,EAAE;IACjB,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,GAAG,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;QACxE,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACrF,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE;QACzB,GAAG,OAAO;QACV,cAAc,EAAE,IAAI;QACpB,IAAI,EAAE,iBAAiB;QACvB,GAAG,CAAC,QAAQ,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC;KAClD,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,QAAQ,CAAC,GAAG,EAAE,CAAC;QACf,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,UAAU,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/G,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3C,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAY,EAAU,EAAE,CACzD,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;IACvC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAC5D,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QAC9B,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACjD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IAChC,CAAC;SAAM,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAClC,MAAM,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC;SAAM,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACvC,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IAC/E,CAAC;SAAM,CAAC;QACN,MAAM,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC,CAAC,CAAC;AASL,6BAA6B;AAC7B,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,EAAE,EAC1B,IAAI,GAAG,KAAK,EACZ,IAAI,GAAG,IAAI,MACK,EAAE,EAA4C,EAAE;IAChE,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3E,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,KAAK,EAAE,EAAE,CAAC;AACtD,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coralreefventures/site-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Command-line tools for a Coral Reef public site: serve the static export as the host does,
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Command-line tools for a Coral Reef public site: serve the static export as the host does, gate it with Lighthouse, and lock it behind the door.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"keywords": [
|
|
20
20
|
"static-site",
|
|
21
21
|
"lighthouse",
|
|
22
|
+
"amplify",
|
|
22
23
|
"coral-reef"
|
|
23
24
|
],
|
|
24
25
|
"files": [
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
],
|
|
29
30
|
"bin": {
|
|
30
31
|
"reef-serve": "./dist/bin/reef-serve.js",
|
|
31
|
-
"reef-lighthouse": "./dist/bin/reef-lighthouse.js"
|
|
32
|
+
"reef-lighthouse": "./dist/bin/reef-lighthouse.js",
|
|
33
|
+
"reef-door-bundle": "./dist/bin/reef-door-bundle.js"
|
|
32
34
|
},
|
|
33
35
|
"exports": {
|
|
34
36
|
".": {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
|
|
4
|
+
import { parseSignoutChain } from "../door/config.ts";
|
|
5
|
+
import { bundleDoor } from "../door/package.ts";
|
|
6
|
+
|
|
7
|
+
const usage = `reef-door-bundle --site <id> [--out out] [--dest .amplify-hosting] --door-url <url> --hosts <h1,h2>
|
|
8
|
+
--jwks-url <url> [--signout-chain id=host,id=host] [--host-header host|x-forwarded-host]
|
|
9
|
+
|
|
10
|
+
Locks a static export behind the door: writes .amplify-hosting with the export inside the gate's compute bundle and
|
|
11
|
+
one route, /* to Compute. --door-url, --hosts, --jwks-url, --signout-chain and --host-header fall back to
|
|
12
|
+
CRV_DOOR_URL, CRV_DOOR_HOSTS, CRV_DOOR_JWKS_URL, CRV_DOOR_SIGNOUT_CHAIN and CRV_DOOR_HOST_HEADER.`;
|
|
13
|
+
|
|
14
|
+
const { values } = parseArgs({
|
|
15
|
+
options: {
|
|
16
|
+
site: { type: "string" },
|
|
17
|
+
out: { type: "string", default: "out" },
|
|
18
|
+
dest: { type: "string", default: ".amplify-hosting" },
|
|
19
|
+
"door-url": { type: "string" },
|
|
20
|
+
hosts: { type: "string" },
|
|
21
|
+
"jwks-url": { type: "string" },
|
|
22
|
+
"signout-chain": { type: "string" },
|
|
23
|
+
"host-header": { type: "string" },
|
|
24
|
+
help: { type: "boolean", short: "h" },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (values.help) {
|
|
29
|
+
console.log(usage);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const env = process.env;
|
|
34
|
+
const doorUrl = values["door-url"] ?? env.CRV_DOOR_URL;
|
|
35
|
+
const hosts = values.hosts ?? env.CRV_DOOR_HOSTS;
|
|
36
|
+
const jwksUrl = values["jwks-url"] ?? env.CRV_DOOR_JWKS_URL;
|
|
37
|
+
const hostHeader = values["host-header"] ?? env.CRV_DOOR_HOST_HEADER ?? "host";
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (!values.site || !doorUrl || !hosts || !jwksUrl) {
|
|
41
|
+
throw new Error("--site, --door-url, --hosts and --jwks-url are required (or their CRV_DOOR_* variables)");
|
|
42
|
+
}
|
|
43
|
+
if (hostHeader !== "host" && hostHeader !== "x-forwarded-host") {
|
|
44
|
+
throw new Error(`--host-header must be host or x-forwarded-host: ${hostHeader}`);
|
|
45
|
+
}
|
|
46
|
+
const manifest = await bundleDoor({
|
|
47
|
+
site: values.site,
|
|
48
|
+
out: values.out,
|
|
49
|
+
dest: values.dest,
|
|
50
|
+
doorUrl,
|
|
51
|
+
hosts: hosts.split(","),
|
|
52
|
+
jwksUrl,
|
|
53
|
+
signout: parseSignoutChain(values["signout-chain"] ?? env.CRV_DOOR_SIGNOUT_CHAIN),
|
|
54
|
+
hostHeader,
|
|
55
|
+
});
|
|
56
|
+
console.log(`reef-door-bundle: ${values.dest} written for ${values.site}, gate ${manifest.framework.version}`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`reef-door-bundle: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
@@ -4,9 +4,10 @@ import { parseArgs } from "node:util";
|
|
|
4
4
|
import { runLighthouse } from "../lighthouse.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* reef-lighthouse [--root out] [--reports lighthouse] [--port
|
|
7
|
+
* reef-lighthouse [--root out] [--reports lighthouse] [--port N] [--threshold 90] [--chrome <path>] [path ...]
|
|
8
8
|
*
|
|
9
|
-
* Lighthouse on every page in the export's sitemap, or the paths given, each category at the threshold or above.
|
|
9
|
+
* Lighthouse on every page in the export's sitemap, or the paths given, each category at the threshold or above. The
|
|
10
|
+
* export is served on a free port unless --port pins one (Streamlane's run met EADDRINUSE on a fixed 3160).
|
|
10
11
|
* Exits 1 with the failing pages named when one is under, and leaves their reports in --reports.
|
|
11
12
|
*/
|
|
12
13
|
const { values, positionals } = parseArgs({
|
|
@@ -14,7 +15,7 @@ const { values, positionals } = parseArgs({
|
|
|
14
15
|
options: {
|
|
15
16
|
root: { type: "string", default: "out" },
|
|
16
17
|
reports: { type: "string", default: "lighthouse" },
|
|
17
|
-
port: { type: "string"
|
|
18
|
+
port: { type: "string" },
|
|
18
19
|
threshold: { type: "string", default: "90" },
|
|
19
20
|
chrome: { type: "string" },
|
|
20
21
|
"lighthouse-version": { type: "string", default: "13.5.0" },
|
|
@@ -24,7 +25,7 @@ const { values, positionals } = parseArgs({
|
|
|
24
25
|
|
|
25
26
|
if (values.help) {
|
|
26
27
|
console.log(
|
|
27
|
-
"reef-lighthouse [--root out] [--reports lighthouse] [--port
|
|
28
|
+
"reef-lighthouse [--root out] [--reports lighthouse] [--port N] [--threshold 90] [--chrome <path>] [path ...]",
|
|
28
29
|
);
|
|
29
30
|
process.exit(0);
|
|
30
31
|
}
|
|
@@ -33,7 +34,7 @@ const threshold = Number(values.threshold) / 100;
|
|
|
33
34
|
const results = await runLighthouse({
|
|
34
35
|
root: values.root,
|
|
35
36
|
reportDir: values.reports,
|
|
36
|
-
port: Number(values.port),
|
|
37
|
+
...(values.port !== undefined && { port: Number(values.port) }),
|
|
37
38
|
threshold,
|
|
38
39
|
paths: positionals,
|
|
39
40
|
lighthouseVersion: values["lighthouse-version"],
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { type Jwks, parseJwks } from "./verify.ts";
|
|
4
|
+
|
|
5
|
+
/** One stop on the sign-out walk: a site behind the door, by id, and the host its gate answers on. */
|
|
6
|
+
export type SignoutStop = { id: string; host: string };
|
|
7
|
+
|
|
8
|
+
/** gate.config.json, as reef-door-bundle writes it beside the gate. Data only: it names no product. */
|
|
9
|
+
export type GateConfigFile = {
|
|
10
|
+
version: 1;
|
|
11
|
+
/** This site's id at the door, which the door's redirect names as `?site=`. */
|
|
12
|
+
site: string;
|
|
13
|
+
/** The door's origin, `https://` and nothing after the host. Tickets name it as `iss`. */
|
|
14
|
+
door: string;
|
|
15
|
+
/** The hosts this gate answers on, each exactly as the Host header carries it. Any other gets 403. */
|
|
16
|
+
hosts: string[];
|
|
17
|
+
/** Which header names the host: `host`, or `x-forwarded-host` if the platform rewrites Host. */
|
|
18
|
+
hostHeader: "host" | "x-forwarded-host";
|
|
19
|
+
/** The fixed sign-out walk, in order: the same list the door's own site registry holds. */
|
|
20
|
+
signout: SignoutStop[];
|
|
21
|
+
/** The door's public keys. */
|
|
22
|
+
jwks: { keys: unknown[] };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type GateConfig = Omit<GateConfigFile, "jwks"> & { keys: Jwks; issuer: string };
|
|
26
|
+
|
|
27
|
+
const siteId = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
28
|
+
const hostName = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:\d{1,5})?$/;
|
|
29
|
+
|
|
30
|
+
export const isSiteId = (value: unknown): value is string => typeof value === "string" && siteId.test(value);
|
|
31
|
+
export const isHost = (value: unknown): value is string =>
|
|
32
|
+
typeof value === "string" && value.length <= 253 && hostName.test(value);
|
|
33
|
+
|
|
34
|
+
/** The door's origin from its URL: https only, with no path, query, fragment or credentials. */
|
|
35
|
+
export const doorOrigin = (door: unknown): string => {
|
|
36
|
+
if (typeof door !== "string") throw new Error("the door URL is missing");
|
|
37
|
+
let url: URL;
|
|
38
|
+
try {
|
|
39
|
+
url = new URL(door);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(`the door URL is not a URL: ${door}`);
|
|
42
|
+
}
|
|
43
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
|
|
44
|
+
throw new Error(`the door URL must be an https origin: ${door}`);
|
|
45
|
+
}
|
|
46
|
+
if (url.pathname !== "/") throw new Error(`the door URL must be an origin, with no path: ${door}`);
|
|
47
|
+
return url.origin;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The sign-out walk: unique site ids, each with a host, and nothing else (no URL, so no redirect from data). */
|
|
51
|
+
const checkSignout = (signout: unknown): SignoutStop[] => {
|
|
52
|
+
if (!Array.isArray(signout)) throw new Error("the sign-out walk is not a list");
|
|
53
|
+
const seen = new Set<string>();
|
|
54
|
+
for (const stop of signout as Partial<SignoutStop>[]) {
|
|
55
|
+
if (!isSiteId(stop?.id) || !isHost(stop?.host) || seen.has(stop.id)) {
|
|
56
|
+
throw new Error(`a sign-out stop is not a unique id and a host: ${JSON.stringify(stop)}`);
|
|
57
|
+
}
|
|
58
|
+
seen.add(stop.id);
|
|
59
|
+
}
|
|
60
|
+
return (signout as SignoutStop[]).map(({ id, host }) => ({ id, host }));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks a gate configuration and returns it ready to use, or throws naming what is wrong. The gate calls this as it
|
|
65
|
+
* starts and refuses to start on a bad file; the bundler calls it before it writes one.
|
|
66
|
+
*/
|
|
67
|
+
export const parseGateConfig = (value: unknown): GateConfig => {
|
|
68
|
+
if (typeof value !== "object" || value === null) throw new Error("the gate configuration is not an object");
|
|
69
|
+
const file = value as Partial<GateConfigFile>;
|
|
70
|
+
if (file.version !== 1) throw new Error("the gate configuration has an unknown version");
|
|
71
|
+
if (!isSiteId(file.site)) throw new Error(`the site id is not lowercase letters, digits and hyphens: ${file.site}`);
|
|
72
|
+
const issuer = doorOrigin(file.door);
|
|
73
|
+
if (!Array.isArray(file.hosts) || file.hosts.length === 0) throw new Error("the gate answers on no host");
|
|
74
|
+
for (const host of file.hosts) {
|
|
75
|
+
if (!isHost(host)) throw new Error(`not a lowercase host name: ${host}`);
|
|
76
|
+
}
|
|
77
|
+
if (file.hostHeader !== "host" && file.hostHeader !== "x-forwarded-host") {
|
|
78
|
+
throw new Error(`the host header must be host or x-forwarded-host: ${file.hostHeader}`);
|
|
79
|
+
}
|
|
80
|
+
const signout = checkSignout(file.signout ?? []);
|
|
81
|
+
const keys = parseJwks(file.jwks);
|
|
82
|
+
return {
|
|
83
|
+
version: 1,
|
|
84
|
+
site: file.site,
|
|
85
|
+
door: issuer,
|
|
86
|
+
issuer,
|
|
87
|
+
hosts: [...file.hosts],
|
|
88
|
+
hostHeader: file.hostHeader,
|
|
89
|
+
signout,
|
|
90
|
+
keys,
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Reads and checks gate.config.json. */
|
|
95
|
+
export const readGateConfig = async (file: string | URL): Promise<GateConfig> =>
|
|
96
|
+
parseGateConfig(JSON.parse(await readFile(file, "utf8")));
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `id=host,id=host` as the bin's --signout-chain takes it, in walk order.
|
|
100
|
+
*/
|
|
101
|
+
export const parseSignoutChain = (value: string | undefined): SignoutStop[] =>
|
|
102
|
+
(value ?? "")
|
|
103
|
+
.split(",")
|
|
104
|
+
.map((entry) => entry.trim())
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.map((entry) => {
|
|
107
|
+
const [id = "", host = "", ...rest] = entry.split("=");
|
|
108
|
+
if (rest.length > 0 || !isSiteId(id) || !isHost(host)) {
|
|
109
|
+
throw new Error(`a sign-out stop must be id=host: ${entry}`);
|
|
110
|
+
}
|
|
111
|
+
return { id, host };
|
|
112
|
+
});
|
package/src/door/next.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A backslash or any control character (U+0000 to U+001F, U+007F), which a browser's URL parser reads as something else. */
|
|
2
|
+
export const unsafe = (value: string): boolean => [...value].some((c) => c === "\\" || c <= "\u001f" || c === "\u007f");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* *Canonical next*: the same-origin path and query `next` names on `https://<host>`, re-serialised, or null when it
|
|
6
|
+
* names anything else. "Starts with / and not //" is not enough on its own: browsers resolve `/\evil.com` and
|
|
7
|
+
* `/<TAB>/evil.com` to the host evil.com, `/.//evil.com` normalises to `//evil.com`, and Node's setHeader accepts a
|
|
8
|
+
* tab. So the value is refused on sight if it is long or carries a backslash or control character, parsed against the
|
|
9
|
+
* origin, required to stay on it, and re-serialised; only the re-serialised value is ever used. The door and the
|
|
10
|
+
* ticket issuer run the same steps on the same cases.
|
|
11
|
+
*/
|
|
12
|
+
export const canonicalNext = (next: unknown, host: string): string | null => {
|
|
13
|
+
if (typeof next !== "string" || next.length === 0 || next.length > 512 || unsafe(next)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!next.startsWith("/") || next.startsWith("//")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
let origin: string;
|
|
20
|
+
let url: URL;
|
|
21
|
+
try {
|
|
22
|
+
origin = new URL(`https://${host}`).origin;
|
|
23
|
+
url = new URL(next, origin);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (url.origin !== origin) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const path = url.pathname + url.search;
|
|
31
|
+
if (path.startsWith("//") || unsafe(path)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return path;
|
|
35
|
+
};
|