@coralreefventures/site-tools 0.1.1 → 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.
Files changed (42) hide show
  1. package/README.md +56 -5
  2. package/dist/bin/reef-door-bundle.d.ts +3 -0
  3. package/dist/bin/reef-door-bundle.d.ts.map +1 -0
  4. package/dist/bin/reef-door-bundle.js +56 -0
  5. package/dist/bin/reef-door-bundle.js.map +1 -0
  6. package/dist/door/config.d.ts +44 -0
  7. package/dist/door/config.d.ts.map +1 -0
  8. package/dist/door/config.js +89 -0
  9. package/dist/door/config.js.map +1 -0
  10. package/dist/door/next.d.ts +12 -0
  11. package/dist/door/next.d.ts.map +1 -0
  12. package/dist/door/next.js +36 -0
  13. package/dist/door/next.js.map +1 -0
  14. package/dist/door/package.d.ts +66 -0
  15. package/dist/door/package.d.ts.map +1 -0
  16. package/dist/door/package.js +150 -0
  17. package/dist/door/package.js.map +1 -0
  18. package/dist/door/server.d.ts +48 -0
  19. package/dist/door/server.d.ts.map +1 -0
  20. package/dist/door/server.js +272 -0
  21. package/dist/door/server.js.map +1 -0
  22. package/dist/door/verify.d.ts +52 -0
  23. package/dist/door/verify.d.ts.map +1 -0
  24. package/dist/door/verify.js +133 -0
  25. package/dist/door/verify.js.map +1 -0
  26. package/dist/index.d.ts +6 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/serve.d.ts +26 -1
  31. package/dist/serve.d.ts.map +1 -1
  32. package/dist/serve.js +84 -39
  33. package/dist/serve.js.map +1 -1
  34. package/package.json +5 -3
  35. package/src/bin/reef-door-bundle.ts +60 -0
  36. package/src/door/config.ts +112 -0
  37. package/src/door/next.ts +35 -0
  38. package/src/door/package.ts +184 -0
  39. package/src/door/server.ts +308 -0
  40. package/src/door/verify.ts +152 -0
  41. package/src/index.ts +45 -1
  42. package/src/serve.ts +101 -29
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # @coralreefventures/site-tools
2
2
 
3
- Two command-line tools for a Coral Reef public site, which is a Next.js static export served by Amplify Hosting.
4
- Both sites carried them as `scripts/serve.ts` and `scripts/lighthouse.ts`; they are bins now, and the products' scripts
5
- go.
3
+ Three command-line tools for a Coral Reef public site, which is a Next.js static export served by Amplify Hosting.
4
+ Both sites carried the first two as `scripts/serve.ts` and `scripts/lighthouse.ts`; they are bins now, and the
5
+ products' scripts go. The third locks a site behind the door.
6
6
 
7
7
  ```sh
8
8
  reef-serve [--root out] [--port 3140]
9
9
  reef-lighthouse [--root out] [--reports lighthouse] [--port 3160] [--threshold 90] [--chrome <path>] [path ...]
10
+ reef-door-bundle --site <id> [--out out] --door-url <url> --hosts <h1,h2> --jwks-url <url> [--signout-chain id=host,...]
10
11
  ```
11
12
 
12
13
  `reef-serve` serves the export the way Amplify does, for Playwright and Lighthouse: a directory answers with its
@@ -25,8 +26,58 @@ bundles axe-core (MPL-2.0), which is outside the family's licence allowlist; it
25
26
  (`--lighthouse-version`). Chrome is Playwright's Chromium when `@playwright/test` is installed beside this package,
26
27
  or `--chrome`.
27
28
 
29
+ ## The door gate
30
+
31
+ `reef-door-bundle` turns a static export into an Amplify deployment that serves nothing without a session from the
32
+ door. It writes `.amplify-hosting/` with the export inside the gate's compute bundle and a deployment manifest with
33
+ exactly one route, `/*` to Compute, and no static route, so every byte (pages, RSC payloads, `_next/static` chunks,
34
+ images, social cards, the 404 page) is answered by the gate. The app runs on platform `WEB_COMPUTE` with the cache
35
+ type `AMPLIFY_MANAGED`, so cookies are in the CDN's cache key.
36
+
37
+ ```sh
38
+ reef-door-bundle --site <id> --door-url https://<door> --hosts <host> --jwks-url https://<door>/.well-known/crv-door-jwks.json \
39
+ --signout-chain <id>=<host>,<id>=<host>
40
+ ```
41
+
42
+ Each flag falls back to an environment variable, so a build spec can set them per app: `CRV_DOOR_URL`,
43
+ `CRV_DOOR_HOSTS`, `CRV_DOOR_JWKS_URL`, `CRV_DOOR_SIGNOUT_CHAIN` and `CRV_DOOR_HOST_HEADER` (`host`, the default, or
44
+ `x-forwarded-host` if the platform rewrites Host). The bin fetches the JWKS and fails the build if it is unreachable,
45
+ empty, or anything but P-256 public keys; it refuses a `static/` directory, a second route and a bundle over 200 MB.
46
+
47
+ The gate is a Node server with no dependencies (it imports Node's own modules and nothing else, which a test holds),
48
+ listening on port 3000. For every request, in order:
49
+
50
+ 1. A path with `..`, a backslash or a control character, raw or percent-encoded, is a 400; a method other than GET,
51
+ HEAD or POST is a 405.
52
+ 2. A Host not in `--hosts` is an empty 403.
53
+ 3. `/robots.txt` disallows everything; `/_door/health` answers `ok`.
54
+ 4. `POST /_door` takes the door's ticket from a form body: an ES256 JWS whose `kid` is in the JWKS, issued by the door
55
+ for this exact host, unexpired and living at most an hour, bound to the `__Host-crv_door_state` cookie, and naming
56
+ the same canonical `next` as the form. It becomes the `__Host-crv_door` cookie (`HttpOnly; Secure; SameSite=Lax`,
57
+ for the ticket's remaining life) and a 303 to `next`. Anything wrong is a 303 back to the door with `?error=ticket`.
58
+ 5. `/_door/signout?then=<id>` clears both cookies and goes on by a fixed map: to the gate of the site `<id>` in
59
+ `--signout-chain`, naming the site after it, or, for anything else (`door`, the last stop), to the door's
60
+ `/signout/`. It never redirects to a URL from the query.
61
+ 6. With a valid session the export is served as Amplify maps it, HTML and `.txt` as `private, no-store` and
62
+ `/_next/static/**` as `private, max-age=31536000, immutable`.
63
+ 7. Without one, a page load gets a fresh state cookie and a 302 to the door with `site`, `host`, `next` and `state`;
64
+ anything else gets an empty 401.
65
+ 8. An exception is an empty 500. The gate fails closed.
66
+
67
+ Every response carries `X-Robots-Tag: noindex, nofollow`, HSTS, `nosniff`, `X-Frame-Options: DENY` and a
68
+ `Cache-Control` with `no-store` or `private`. `next` is canonicalised by parsing it against the host, re-serialising
69
+ it as path and query, and refusing it if it leaves the origin or holds a backslash or control character
70
+ (`canonicalNext`, exported, so the door's ticket issuer can run the same steps).
71
+
72
+ The gate holds only public keys, takes all of its configuration as data and names no product. The ticket issuer, the
73
+ door and the site registry are the door's own.
74
+
75
+ ## The library
76
+
28
77
  The library entry exports `serve`, `createStaticServer`, `runLighthouse` and `pagesFromSitemap` for a script that
29
- wants them in-process.
78
+ wants them in-process, and the gate's parts: `createGate`, `startGate`, `verifyTicket`, `parseJwks`,
79
+ `canonicalNext`, `parseGateConfig` and `bundleDoor`.
30
80
 
31
81
  This is the one package in the repository with a build: a bin has to be JavaScript, because Node refuses to strip
32
- types from a file under `node_modules`. `tsc` writes `dist/` on `prepack`.
82
+ types from a file under `node_modules`. `tsc` writes `dist/` on `prepack`, and the door bundle copies the gate's
83
+ compiled files from there.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=reef-door-bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reef-door-bundle.d.ts","sourceRoot":"","sources":["../../src/bin/reef-door-bundle.ts"],"names":[],"mappings":""}
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { parseSignoutChain } from "../door/config.js";
4
+ import { bundleDoor } from "../door/package.js";
5
+ const usage = `reef-door-bundle --site <id> [--out out] [--dest .amplify-hosting] --door-url <url> --hosts <h1,h2>
6
+ --jwks-url <url> [--signout-chain id=host,id=host] [--host-header host|x-forwarded-host]
7
+
8
+ Locks a static export behind the door: writes .amplify-hosting with the export inside the gate's compute bundle and
9
+ one route, /* to Compute. --door-url, --hosts, --jwks-url, --signout-chain and --host-header fall back to
10
+ CRV_DOOR_URL, CRV_DOOR_HOSTS, CRV_DOOR_JWKS_URL, CRV_DOOR_SIGNOUT_CHAIN and CRV_DOOR_HOST_HEADER.`;
11
+ const { values } = parseArgs({
12
+ options: {
13
+ site: { type: "string" },
14
+ out: { type: "string", default: "out" },
15
+ dest: { type: "string", default: ".amplify-hosting" },
16
+ "door-url": { type: "string" },
17
+ hosts: { type: "string" },
18
+ "jwks-url": { type: "string" },
19
+ "signout-chain": { type: "string" },
20
+ "host-header": { type: "string" },
21
+ help: { type: "boolean", short: "h" },
22
+ },
23
+ });
24
+ if (values.help) {
25
+ console.log(usage);
26
+ process.exit(0);
27
+ }
28
+ const env = process.env;
29
+ const doorUrl = values["door-url"] ?? env.CRV_DOOR_URL;
30
+ const hosts = values.hosts ?? env.CRV_DOOR_HOSTS;
31
+ const jwksUrl = values["jwks-url"] ?? env.CRV_DOOR_JWKS_URL;
32
+ const hostHeader = values["host-header"] ?? env.CRV_DOOR_HOST_HEADER ?? "host";
33
+ try {
34
+ if (!values.site || !doorUrl || !hosts || !jwksUrl) {
35
+ throw new Error("--site, --door-url, --hosts and --jwks-url are required (or their CRV_DOOR_* variables)");
36
+ }
37
+ if (hostHeader !== "host" && hostHeader !== "x-forwarded-host") {
38
+ throw new Error(`--host-header must be host or x-forwarded-host: ${hostHeader}`);
39
+ }
40
+ const manifest = await bundleDoor({
41
+ site: values.site,
42
+ out: values.out,
43
+ dest: values.dest,
44
+ doorUrl,
45
+ hosts: hosts.split(","),
46
+ jwksUrl,
47
+ signout: parseSignoutChain(values["signout-chain"] ?? env.CRV_DOOR_SIGNOUT_CHAIN),
48
+ hostHeader,
49
+ });
50
+ console.log(`reef-door-bundle: ${values.dest} written for ${values.site}, gate ${manifest.framework.version}`);
51
+ }
52
+ catch (error) {
53
+ console.error(`reef-door-bundle: ${error instanceof Error ? error.message : String(error)}`);
54
+ process.exit(1);
55
+ }
56
+ //# sourceMappingURL=reef-door-bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reef-door-bundle.js","sourceRoot":"","sources":["../../src/bin/reef-door-bundle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { type Jwks } from "./verify.ts";
2
+ /** One stop on the sign-out walk: a site behind the door, by id, and the host its gate answers on. */
3
+ export type SignoutStop = {
4
+ id: string;
5
+ host: string;
6
+ };
7
+ /** gate.config.json, as reef-door-bundle writes it beside the gate. Data only: it names no product. */
8
+ export type GateConfigFile = {
9
+ version: 1;
10
+ /** This site's id at the door, which the door's redirect names as `?site=`. */
11
+ site: string;
12
+ /** The door's origin, `https://` and nothing after the host. Tickets name it as `iss`. */
13
+ door: string;
14
+ /** The hosts this gate answers on, each exactly as the Host header carries it. Any other gets 403. */
15
+ hosts: string[];
16
+ /** Which header names the host: `host`, or `x-forwarded-host` if the platform rewrites Host. */
17
+ hostHeader: "host" | "x-forwarded-host";
18
+ /** The fixed sign-out walk, in order: the same list the door's own site registry holds. */
19
+ signout: SignoutStop[];
20
+ /** The door's public keys. */
21
+ jwks: {
22
+ keys: unknown[];
23
+ };
24
+ };
25
+ export type GateConfig = Omit<GateConfigFile, "jwks"> & {
26
+ keys: Jwks;
27
+ issuer: string;
28
+ };
29
+ export declare const isSiteId: (value: unknown) => value is string;
30
+ export declare const isHost: (value: unknown) => value is string;
31
+ /** The door's origin from its URL: https only, with no path, query, fragment or credentials. */
32
+ export declare const doorOrigin: (door: unknown) => string;
33
+ /**
34
+ * Checks a gate configuration and returns it ready to use, or throws naming what is wrong. The gate calls this as it
35
+ * starts and refuses to start on a bad file; the bundler calls it before it writes one.
36
+ */
37
+ export declare const parseGateConfig: (value: unknown) => GateConfig;
38
+ /** Reads and checks gate.config.json. */
39
+ export declare const readGateConfig: (file: string | URL) => Promise<GateConfig>;
40
+ /**
41
+ * `id=host,id=host` as the bin's --signout-chain takes it, in walk order.
42
+ */
43
+ export declare const parseSignoutChain: (value: string | undefined) => SignoutStop[];
44
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/door/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,IAAI,EAAa,MAAM,aAAa,CAAC;AAEnD,sGAAsG;AACtG,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,uGAAuG;AACvG,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,CAAC,CAAC;IACX,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb,sGAAsG;IACtG,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,gGAAgG;IAChG,UAAU,EAAE,MAAM,GAAG,kBAAkB,CAAC;IACxC,2FAA2F;IAC3F,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,8BAA8B;IAC9B,IAAI,EAAE;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAKvF,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAAyD,CAAC;AAC7G,eAAO,MAAM,MAAM,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACyB,CAAC;AAE3E,gGAAgG;AAChG,eAAO,MAAM,UAAU,GAAI,MAAM,OAAO,KAAG,MAa1C,CAAC;AAeF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,OAAO,KAAG,UAyBhD,CAAC;AAEF,yCAAyC;AACzC,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,GAAG,GAAG,KAAG,OAAO,CAAC,UAAU,CACjB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,WAAW,EAWnE,CAAC"}
@@ -0,0 +1,89 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseJwks } from "./verify.js";
3
+ const siteId = /^[a-z0-9][a-z0-9-]{0,31}$/;
4
+ const hostName = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:\d{1,5})?$/;
5
+ export const isSiteId = (value) => typeof value === "string" && siteId.test(value);
6
+ export const isHost = (value) => typeof value === "string" && value.length <= 253 && hostName.test(value);
7
+ /** The door's origin from its URL: https only, with no path, query, fragment or credentials. */
8
+ export const doorOrigin = (door) => {
9
+ if (typeof door !== "string")
10
+ throw new Error("the door URL is missing");
11
+ let url;
12
+ try {
13
+ url = new URL(door);
14
+ }
15
+ catch {
16
+ throw new Error(`the door URL is not a URL: ${door}`);
17
+ }
18
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
19
+ throw new Error(`the door URL must be an https origin: ${door}`);
20
+ }
21
+ if (url.pathname !== "/")
22
+ throw new Error(`the door URL must be an origin, with no path: ${door}`);
23
+ return url.origin;
24
+ };
25
+ /** The sign-out walk: unique site ids, each with a host, and nothing else (no URL, so no redirect from data). */
26
+ const checkSignout = (signout) => {
27
+ if (!Array.isArray(signout))
28
+ throw new Error("the sign-out walk is not a list");
29
+ const seen = new Set();
30
+ for (const stop of signout) {
31
+ if (!isSiteId(stop?.id) || !isHost(stop?.host) || seen.has(stop.id)) {
32
+ throw new Error(`a sign-out stop is not a unique id and a host: ${JSON.stringify(stop)}`);
33
+ }
34
+ seen.add(stop.id);
35
+ }
36
+ return signout.map(({ id, host }) => ({ id, host }));
37
+ };
38
+ /**
39
+ * Checks a gate configuration and returns it ready to use, or throws naming what is wrong. The gate calls this as it
40
+ * starts and refuses to start on a bad file; the bundler calls it before it writes one.
41
+ */
42
+ export const parseGateConfig = (value) => {
43
+ if (typeof value !== "object" || value === null)
44
+ throw new Error("the gate configuration is not an object");
45
+ const file = value;
46
+ if (file.version !== 1)
47
+ throw new Error("the gate configuration has an unknown version");
48
+ if (!isSiteId(file.site))
49
+ throw new Error(`the site id is not lowercase letters, digits and hyphens: ${file.site}`);
50
+ const issuer = doorOrigin(file.door);
51
+ if (!Array.isArray(file.hosts) || file.hosts.length === 0)
52
+ throw new Error("the gate answers on no host");
53
+ for (const host of file.hosts) {
54
+ if (!isHost(host))
55
+ throw new Error(`not a lowercase host name: ${host}`);
56
+ }
57
+ if (file.hostHeader !== "host" && file.hostHeader !== "x-forwarded-host") {
58
+ throw new Error(`the host header must be host or x-forwarded-host: ${file.hostHeader}`);
59
+ }
60
+ const signout = checkSignout(file.signout ?? []);
61
+ const keys = parseJwks(file.jwks);
62
+ return {
63
+ version: 1,
64
+ site: file.site,
65
+ door: issuer,
66
+ issuer,
67
+ hosts: [...file.hosts],
68
+ hostHeader: file.hostHeader,
69
+ signout,
70
+ keys,
71
+ };
72
+ };
73
+ /** Reads and checks gate.config.json. */
74
+ export const readGateConfig = async (file) => parseGateConfig(JSON.parse(await readFile(file, "utf8")));
75
+ /**
76
+ * `id=host,id=host` as the bin's --signout-chain takes it, in walk order.
77
+ */
78
+ export const parseSignoutChain = (value) => (value ?? "")
79
+ .split(",")
80
+ .map((entry) => entry.trim())
81
+ .filter(Boolean)
82
+ .map((entry) => {
83
+ const [id = "", host = "", ...rest] = entry.split("=");
84
+ if (rest.length > 0 || !isSiteId(id) || !isHost(host)) {
85
+ throw new Error(`a sign-out stop must be id=host: ${entry}`);
86
+ }
87
+ return { id, host };
88
+ });
89
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/door/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
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 declare const unsafe: (value: string) => boolean;
3
+ /**
4
+ * *Canonical next*: the same-origin path and query `next` names on `https://<host>`, re-serialised, or null when it
5
+ * names anything else. "Starts with / and not //" is not enough on its own: browsers resolve `/\evil.com` and
6
+ * `/<TAB>/evil.com` to the host evil.com, `/.//evil.com` normalises to `//evil.com`, and Node's setHeader accepts a
7
+ * tab. So the value is refused on sight if it is long or carries a backslash or control character, parsed against the
8
+ * origin, required to stay on it, and re-serialised; only the re-serialised value is ever used. The door and the
9
+ * ticket issuer run the same steps on the same cases.
10
+ */
11
+ export declare const canonicalNext: (next: unknown, host: string) => string | null;
12
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../../src/door/next.ts"],"names":[],"mappings":"AAAA,6HAA6H;AAC7H,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,KAAG,OAAgF,CAAC;AAExH;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,KAAG,MAAM,GAAG,IAuBpE,CAAC"}
@@ -0,0 +1,36 @@
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) => [...value].some((c) => c === "\\" || c <= "\u001f" || c === "\u007f");
3
+ /**
4
+ * *Canonical next*: the same-origin path and query `next` names on `https://<host>`, re-serialised, or null when it
5
+ * names anything else. "Starts with / and not //" is not enough on its own: browsers resolve `/\evil.com` and
6
+ * `/<TAB>/evil.com` to the host evil.com, `/.//evil.com` normalises to `//evil.com`, and Node's setHeader accepts a
7
+ * tab. So the value is refused on sight if it is long or carries a backslash or control character, parsed against the
8
+ * origin, required to stay on it, and re-serialised; only the re-serialised value is ever used. The door and the
9
+ * ticket issuer run the same steps on the same cases.
10
+ */
11
+ export const canonicalNext = (next, host) => {
12
+ if (typeof next !== "string" || next.length === 0 || next.length > 512 || unsafe(next)) {
13
+ return null;
14
+ }
15
+ if (!next.startsWith("/") || next.startsWith("//")) {
16
+ return null;
17
+ }
18
+ let origin;
19
+ let url;
20
+ try {
21
+ origin = new URL(`https://${host}`).origin;
22
+ url = new URL(next, origin);
23
+ }
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
+ };
36
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.js","sourceRoot":"","sources":["../../src/door/next.ts"],"names":[],"mappings":"AAAA,6HAA6H;AAC7H,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAa,EAAW,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC;AAExH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAa,EAAE,IAAY,EAAiB,EAAE;IAC1E,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACvF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAc,CAAC;IACnB,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAC3C,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC;IACvC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC"}
@@ -0,0 +1,66 @@
1
+ import { type GateConfigFile, type SignoutStop } from "./config.ts";
2
+ /**
3
+ * The gate's own files, relative to the package's compiled root, which the bundle carries beside server.mjs. They
4
+ * import Node's modules and each other and nothing else, so the bundle needs no node_modules (a test holds the list
5
+ * against the imports).
6
+ */
7
+ export declare const gateFiles: string[];
8
+ /** Amplify takes a deployment of up to 220 MB; the gate refuses one over 200, which leaves room. */
9
+ export declare const maxBundleBytes: number;
10
+ export type DeployManifest = {
11
+ version: 1;
12
+ routes: {
13
+ path: string;
14
+ target: {
15
+ kind: string;
16
+ src?: string;
17
+ };
18
+ }[];
19
+ computeResources: {
20
+ name: string;
21
+ runtime: string;
22
+ entrypoint: string;
23
+ }[];
24
+ framework: {
25
+ name: string;
26
+ version: string;
27
+ };
28
+ };
29
+ export type BundleDoorOptions = {
30
+ /** This site's id at the door. */
31
+ site: string;
32
+ /** The static export to lock: `out` by default. */
33
+ out?: string;
34
+ /** Where Amplify reads the deployment from: `.amplify-hosting`, which is also the only name accepted. */
35
+ dest?: string;
36
+ /** The door's origin. */
37
+ doorUrl: string;
38
+ /** The hosts the gate answers on. */
39
+ hosts: string[];
40
+ /** The door's JWKS: `https:`, or `file:` for a build without the network and for tests. */
41
+ jwksUrl: string;
42
+ /** The sign-out walk, in order. */
43
+ signout?: SignoutStop[];
44
+ hostHeader?: GateConfigFile["hostHeader"];
45
+ /** The compiled package root the gate's files are copied from; this package's own `dist` by default. */
46
+ gateRoot?: string;
47
+ maxBytes?: number;
48
+ };
49
+ /** Fetches the door's JWKS and checks it is P-256 public keys; a build with no usable key fails here. */
50
+ export declare const fetchJwks: (jwksUrl: string) => Promise<{
51
+ keys: unknown[];
52
+ }>;
53
+ /**
54
+ * Checks a written deployment the way the lock depends on it: exactly one route, `/*` to Compute, no static/
55
+ * directory for Amplify to serve around the gate, the gate's entry point present, and the whole under the size limit.
56
+ * Throws naming the first thing wrong.
57
+ */
58
+ export declare const checkDeployment: (dest: string, maxBytes?: number) => Promise<DeployManifest>;
59
+ /**
60
+ * Writes `.amplify-hosting` for a locked site: the static export under compute/default/site, the gate beside it with
61
+ * its configuration, and a deployment manifest with one route, `/*` to Compute, so Amplify serves every byte through
62
+ * the gate. The JWKS is fetched and checked first, and the result is checked after, so a build that would ship an open
63
+ * or unusable gate fails instead.
64
+ */
65
+ export declare const bundleDoor: (options: BundleDoorOptions) => Promise<DeployManifest>;
66
+ //# sourceMappingURL=package.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/door/package.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAmB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAGrF;;;;GAIG;AACH,eAAO,MAAM,SAAS,UAAqF,CAAC;AAE5G,oGAAoG;AACpG,eAAO,MAAM,cAAc,QAAoB,CAAC;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,EAAE,CAAC;IACnE,gBAAgB,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1E,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yGAAyG;IACzG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,2FAA2F;IAC3F,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;IAC1C,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAQF,yGAAyG;AACzG,eAAO,MAAM,SAAS,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,EAAE,CAAA;CAAE,CAiC5E,CAAC;AAUF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAU,MAAM,MAAM,EAAE,iBAAyB,KAAG,OAAO,CAAC,cAAc,CAiBrG,CAAC;AAeF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAU,SAAS,iBAAiB,KAAG,OAAO,CAAC,cAAc,CA6CnF,CAAC"}
@@ -0,0 +1,150 @@
1
+ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseGateConfig } from "./config.js";
5
+ import { parseJwks } from "./verify.js";
6
+ /**
7
+ * The gate's own files, relative to the package's compiled root, which the bundle carries beside server.mjs. They
8
+ * import Node's modules and each other and nothing else, so the bundle needs no node_modules (a test holds the list
9
+ * against the imports).
10
+ */
11
+ export const gateFiles = ["serve.js", "door/server.js", "door/config.js", "door/next.js", "door/verify.js"];
12
+ /** Amplify takes a deployment of up to 220 MB; the gate refuses one over 200, which leaves room. */
13
+ export const maxBundleBytes = 200 * 1024 * 1024;
14
+ const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
15
+ const compiledRoot = fileURLToPath(new URL("../", import.meta.url));
16
+ const ownVersion = async () => JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8")).version;
17
+ /** Fetches the door's JWKS and checks it is P-256 public keys; a build with no usable key fails here. */
18
+ export const fetchJwks = async (jwksUrl) => {
19
+ let url;
20
+ try {
21
+ url = new URL(jwksUrl);
22
+ }
23
+ catch {
24
+ throw new Error(`the JWKS URL is not a URL: ${jwksUrl}`);
25
+ }
26
+ let text;
27
+ if (url.protocol === "file:") {
28
+ text = await readFile(url, "utf8");
29
+ }
30
+ else if (url.protocol === "https:") {
31
+ const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(15_000) }).catch((error) => {
32
+ // fetch says only "fetch failed"; what went wrong (refused, a certificate, a timeout) is its cause.
33
+ const cause = error instanceof Error && error.cause instanceof Error ? `: ${error.cause.message}` : "";
34
+ throw new Error(`the JWKS is unreachable at ${jwksUrl}: ${error instanceof Error ? error.message : error}${cause}`);
35
+ });
36
+ if (!response.ok)
37
+ throw new Error(`the JWKS answered ${response.status} at ${jwksUrl}`);
38
+ text = await response.text();
39
+ }
40
+ else {
41
+ throw new Error(`the JWKS URL must be https: (or file:): ${jwksUrl}`);
42
+ }
43
+ let jwks;
44
+ try {
45
+ jwks = JSON.parse(text);
46
+ }
47
+ catch {
48
+ throw new Error(`the JWKS at ${jwksUrl} is not JSON`);
49
+ }
50
+ parseJwks(jwks);
51
+ return { keys: jwks.keys };
52
+ };
53
+ const sizeOf = async (target) => {
54
+ const info = await stat(target);
55
+ if (!info.isDirectory())
56
+ return info.size;
57
+ let total = 0;
58
+ for (const entry of await readdir(target))
59
+ total += await sizeOf(path.join(target, entry));
60
+ return total;
61
+ };
62
+ /**
63
+ * Checks a written deployment the way the lock depends on it: exactly one route, `/*` to Compute, no static/
64
+ * directory for Amplify to serve around the gate, the gate's entry point present, and the whole under the size limit.
65
+ * Throws naming the first thing wrong.
66
+ */
67
+ export const checkDeployment = async (dest, maxBytes = maxBundleBytes) => {
68
+ const manifest = JSON.parse(await readFile(path.join(dest, "deploy-manifest.json"), "utf8"));
69
+ const [route, ...others] = manifest.routes ?? [];
70
+ if (others.length > 0)
71
+ throw new Error("the deployment has more than one route; only /* to Compute may exist");
72
+ if (route?.path !== "/*" || route.target?.kind !== "Compute" || route.target.src !== "default") {
73
+ throw new Error("the deployment's one route must be /* to Compute default");
74
+ }
75
+ const [compute, ...moreCompute] = manifest.computeResources ?? [];
76
+ if (moreCompute.length > 0 || compute?.name !== "default" || compute.entrypoint !== "server.mjs") {
77
+ throw new Error("the deployment must have one compute resource, default, entered at server.mjs");
78
+ }
79
+ const entries = await readdir(dest);
80
+ if (entries.includes("static"))
81
+ throw new Error("the deployment has a static/ directory, which bypasses the gate");
82
+ await stat(path.join(dest, "compute", "default", "server.mjs"));
83
+ const size = await sizeOf(dest);
84
+ if (size > maxBytes)
85
+ throw new Error(`the deployment is ${size} bytes, over the ${maxBytes} limit`);
86
+ return manifest;
87
+ };
88
+ const entryPoint = (version) => `// Written by reef-door-bundle (@coralreefventures/site-tools ${version}). Do not edit.
89
+ // Every request to this site passes through the gate; nothing is served around it.
90
+ import { startGate } from "./gate/door/server.js";
91
+
92
+ await startGate({
93
+ config: new URL("./gate.config.json", import.meta.url),
94
+ site: new URL("./site/", import.meta.url),
95
+ port: Number(process.env.PORT || 3000),
96
+ });
97
+ `;
98
+ /**
99
+ * Writes `.amplify-hosting` for a locked site: the static export under compute/default/site, the gate beside it with
100
+ * its configuration, and a deployment manifest with one route, `/*` to Compute, so Amplify serves every byte through
101
+ * the gate. The JWKS is fetched and checked first, and the result is checked after, so a build that would ship an open
102
+ * or unusable gate fails instead.
103
+ */
104
+ export const bundleDoor = async (options) => {
105
+ const out = path.resolve(options.out ?? "out");
106
+ const dest = path.resolve(options.dest ?? ".amplify-hosting");
107
+ if (path.basename(dest) !== ".amplify-hosting") {
108
+ throw new Error(`the deployment directory must be named .amplify-hosting, which Amplify reads: ${dest}`);
109
+ }
110
+ if (!(await stat(out).catch(() => null))?.isDirectory())
111
+ throw new Error(`the static export is missing: ${out}`);
112
+ if (out === dest || out.startsWith(`${dest}${path.sep}`) || dest.startsWith(`${out}${path.sep}`)) {
113
+ throw new Error("the static export and the deployment directory must not contain each other");
114
+ }
115
+ const jwks = await fetchJwks(options.jwksUrl);
116
+ const configFile = {
117
+ version: 1,
118
+ site: options.site,
119
+ door: options.doorUrl.replace(/\/$/, ""),
120
+ hosts: options.hosts.map((host) => host.trim().toLowerCase()).filter(Boolean),
121
+ hostHeader: options.hostHeader ?? "host",
122
+ signout: options.signout ?? [],
123
+ jwks,
124
+ };
125
+ const config = parseGateConfig(configFile);
126
+ configFile.door = config.door;
127
+ const gateRoot = options.gateRoot ?? compiledRoot;
128
+ const version = await ownVersion();
129
+ const compute = path.join(dest, "compute", "default");
130
+ await rm(dest, { recursive: true, force: true });
131
+ await mkdir(path.join(compute, "gate"), { recursive: true });
132
+ for (const file of gateFiles) {
133
+ await mkdir(path.dirname(path.join(compute, "gate", file)), { recursive: true });
134
+ await cp(path.join(gateRoot, file), path.join(compute, "gate", file));
135
+ }
136
+ // The gate's files are ES modules named .js, and nothing above them in the bundle says so.
137
+ await writeFile(path.join(compute, "gate", "package.json"), `${JSON.stringify({ type: "module" })}\n`);
138
+ await writeFile(path.join(compute, "server.mjs"), entryPoint(version));
139
+ await writeFile(path.join(compute, "gate.config.json"), `${JSON.stringify(configFile, null, 2)}\n`);
140
+ await cp(out, path.join(compute, "site"), { recursive: true });
141
+ const manifest = {
142
+ version: 1,
143
+ routes: [{ path: "/*", target: { kind: "Compute", src: "default" } }],
144
+ computeResources: [{ name: "default", runtime: "nodejs24.x", entrypoint: "server.mjs" }],
145
+ framework: { name: "crv-door-gate", version },
146
+ };
147
+ await writeFile(path.join(dest, "deploy-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
148
+ return checkDeployment(dest, options.maxBytes);
149
+ };
150
+ //# sourceMappingURL=package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.js","sourceRoot":"","sources":["../../src/door/package.ts"],"names":[],"mappings":"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"}