@rexezuge/tooling 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +297 -2
- package/dist/eslint.d.ts +120 -0
- package/dist/eslint.d.ts.map +1 -0
- package/dist/eslint.js +551 -0
- package/dist/eslint.js.map +1 -0
- package/dist/functions/pages-proxy.d.ts +113 -0
- package/dist/functions/pages-proxy.d.ts.map +1 -0
- package/dist/functions/pages-proxy.js +131 -0
- package/dist/functions/pages-proxy.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/scripts/backup/d1-target.d.ts +35 -0
- package/dist/scripts/backup/d1-target.d.ts.map +1 -0
- package/dist/scripts/backup/d1-target.js +31 -0
- package/dist/scripts/backup/d1-target.js.map +1 -0
- package/dist/scripts/backup/destination-config.d.ts +61 -0
- package/dist/scripts/backup/destination-config.d.ts.map +1 -0
- package/dist/scripts/backup/destination-config.js +57 -0
- package/dist/scripts/backup/destination-config.js.map +1 -0
- package/dist/scripts/backup/encrypt-backup.d.ts +35 -0
- package/dist/scripts/backup/encrypt-backup.d.ts.map +1 -0
- package/dist/scripts/backup/encrypt-backup.js +98 -0
- package/dist/scripts/backup/encrypt-backup.js.map +1 -0
- package/dist/scripts/backup/naming.d.ts +44 -0
- package/dist/scripts/backup/naming.d.ts.map +1 -0
- package/dist/scripts/backup/naming.js +58 -0
- package/dist/scripts/backup/naming.js.map +1 -0
- package/dist/scripts/check-god-files.d.ts +164 -0
- package/dist/scripts/check-god-files.d.ts.map +1 -0
- package/dist/scripts/check-god-files.js +271 -0
- package/dist/scripts/check-god-files.js.map +1 -0
- package/dist/scripts/ensure-spa-shell-stub.d.ts +3 -0
- package/dist/scripts/ensure-spa-shell-stub.d.ts.map +1 -0
- package/dist/scripts/ensure-spa-shell-stub.js +30 -0
- package/dist/scripts/ensure-spa-shell-stub.js.map +1 -0
- package/dist/scripts/init-secrets.d.ts +63 -0
- package/dist/scripts/init-secrets.d.ts.map +1 -0
- package/dist/scripts/init-secrets.js +240 -0
- package/dist/scripts/init-secrets.js.map +1 -0
- package/dist/scripts/lib/cli-args.d.ts +78 -0
- package/dist/scripts/lib/cli-args.d.ts.map +1 -0
- package/dist/scripts/lib/cli-args.js +116 -0
- package/dist/scripts/lib/cli-args.js.map +1 -0
- package/dist/scripts/lib/github-actions.d.ts +26 -0
- package/dist/scripts/lib/github-actions.d.ts.map +1 -0
- package/dist/scripts/lib/github-actions.js +38 -0
- package/dist/scripts/lib/github-actions.js.map +1 -0
- package/dist/scripts/lib/wrangler-table.d.ts +46 -0
- package/dist/scripts/lib/wrangler-table.d.ts.map +1 -0
- package/dist/scripts/lib/wrangler-table.js +99 -0
- package/dist/scripts/lib/wrangler-table.js.map +1 -0
- package/dist/scripts/migrations-lock.d.ts +3 -0
- package/dist/scripts/migrations-lock.d.ts.map +1 -0
- package/dist/scripts/migrations-lock.js +46 -0
- package/dist/scripts/migrations-lock.js.map +1 -0
- package/dist/scripts/prepare-wrangler-config.d.ts +3 -0
- package/dist/scripts/prepare-wrangler-config.d.ts.map +1 -0
- package/dist/scripts/prepare-wrangler-config.js +50 -0
- package/dist/scripts/prepare-wrangler-config.js.map +1 -0
- package/dist/scripts/spa-shell.d.ts +41 -0
- package/dist/scripts/spa-shell.d.ts.map +1 -0
- package/dist/scripts/spa-shell.js +155 -0
- package/dist/scripts/spa-shell.js.map +1 -0
- package/dist/scripts/validate-locales.d.ts +26 -0
- package/dist/scripts/validate-locales.d.ts.map +1 -0
- package/dist/scripts/validate-locales.js +350 -0
- package/dist/scripts/validate-locales.js.map +1 -0
- package/dist/scripts/verify-migrations.d.ts +62 -0
- package/dist/scripts/verify-migrations.d.ts.map +1 -0
- package/dist/scripts/verify-migrations.js +302 -0
- package/dist/scripts/verify-migrations.js.map +1 -0
- package/dist/scripts/verify-spa-shell.d.ts +3 -0
- package/dist/scripts/verify-spa-shell.d.ts.map +1 -0
- package/dist/scripts/verify-spa-shell.js +53 -0
- package/dist/scripts/verify-spa-shell.js.map +1 -0
- package/dist/scripts/wrangler-config/cli.d.ts +22 -0
- package/dist/scripts/wrangler-config/cli.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/cli.js +51 -0
- package/dist/scripts/wrangler-config/cli.js.map +1 -0
- package/dist/scripts/wrangler-config/patches.d.ts +51 -0
- package/dist/scripts/wrangler-config/patches.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/patches.js +140 -0
- package/dist/scripts/wrangler-config/patches.js.map +1 -0
- package/dist/scripts/wrangler-config/resources.d.ts +70 -0
- package/dist/scripts/wrangler-config/resources.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/resources.js +290 -0
- package/dist/scripts/wrangler-config/resources.js.map +1 -0
- package/dist/scripts/wrangler-config/types.d.ts +103 -0
- package/dist/scripts/wrangler-config/types.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/types.js +49 -0
- package/dist/scripts/wrangler-config/types.js.map +1 -0
- package/dist/test/integration-migrations.d.ts +167 -0
- package/dist/test/integration-migrations.d.ts.map +1 -0
- package/dist/test/integration-migrations.js +171 -0
- package/dist/test/integration-migrations.js.map +1 -0
- package/dist/test/mocks/cloudflare-workers.d.ts +106 -0
- package/dist/test/mocks/cloudflare-workers.d.ts.map +1 -0
- package/dist/test/mocks/cloudflare-workers.js +90 -0
- package/dist/test/mocks/cloudflare-workers.js.map +1 -0
- package/dist/vite.d.ts +117 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +125 -0
- package/dist/vite.js.map +1 -0
- package/dist/vitest-web.d.ts +73 -0
- package/dist/vitest-web.d.ts.map +1 -0
- package/dist/vitest-web.js +72 -0
- package/dist/vitest-web.js.map +1 -0
- package/dist/vitest.d.ts +92 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +128 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +58 -3
- package/src/eslint.test.ts +175 -0
- package/src/eslint.ts +640 -0
- package/src/functions/pages-proxy.test.ts +72 -0
- package/src/functions/pages-proxy.ts +187 -0
- package/src/github/actions/retry-step/action.yml +39 -0
- package/src/github/actions/setup-env/action.yml +20 -0
- package/src/github/dependabot.yml +30 -0
- package/src/github/workflows/backup-main.yml +46 -0
- package/src/github/workflows/continuous-deployment.yml +188 -0
- package/src/github/workflows/continuous-integration.yml +259 -0
- package/src/github/workflows/scheduled-version-update.yml +38 -0
- package/src/github/workflows/upstream-sync.yml +56 -0
- package/src/index.ts +42 -0
- package/src/scripts/backup/backup-rules.test.ts +105 -0
- package/src/scripts/backup/d1-target.ts +54 -0
- package/src/scripts/backup/destination-config.ts +92 -0
- package/src/scripts/backup/encrypt-backup.ts +107 -0
- package/src/scripts/backup/naming.ts +62 -0
- package/src/scripts/check-god-files.test.ts +131 -0
- package/src/scripts/check-god-files.ts +327 -0
- package/src/scripts/ensure-spa-shell-stub.ts +34 -0
- package/src/scripts/init-secrets.ts +265 -0
- package/src/scripts/lib/cli-args.ts +154 -0
- package/src/scripts/lib/github-actions.ts +41 -0
- package/src/scripts/lib/wrangler-table.ts +105 -0
- package/src/scripts/migrations-lock.ts +52 -0
- package/src/scripts/prepare-wrangler-config.ts +51 -0
- package/src/scripts/spa-shell.test.ts +91 -0
- package/src/scripts/spa-shell.ts +179 -0
- package/src/scripts/validate-locales.test.ts +89 -0
- package/src/scripts/validate-locales.ts +380 -0
- package/src/scripts/verify-migrations.test.ts +71 -0
- package/src/scripts/verify-migrations.ts +364 -0
- package/src/scripts/verify-spa-shell.ts +56 -0
- package/src/scripts/wrangler-config/cli.ts +51 -0
- package/src/scripts/wrangler-config/patches.ts +157 -0
- package/src/scripts/wrangler-config/resources.ts +330 -0
- package/src/scripts/wrangler-config/types.ts +113 -0
- package/src/test/integration-migrations.test.ts +169 -0
- package/src/test/integration-migrations.ts +267 -0
- package/src/test/mocks/cloudflare-workers.ts +115 -0
- package/src/vite.test.ts +83 -0
- package/src/vite.ts +202 -0
- package/src/vitest-web.ts +109 -0
- package/src/vitest.ts +185 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cloudflare Pages catch-all proxy.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: the `functions/[[path]].ts` shim duplicated in Durable-DAV,
|
|
5
|
+
* Mail-Meow and Edge-Git (and present in nine repos in total). Each copy is an
|
|
6
|
+
* `onRequest` handler that forwards the request to the API worker through a
|
|
7
|
+
* service binding, rewriting the three headers the worker routes on.
|
|
8
|
+
*
|
|
9
|
+
* What each source contributes:
|
|
10
|
+
*
|
|
11
|
+
* - **Durable-DAV / Edge-Git** — the core shape: set `X-Forwarded-Host`,
|
|
12
|
+
* `X-Forwarded-Proto` and `X-Forwarded-Uri` from the original URL, forward the
|
|
13
|
+
* body for non-GET/HEAD methods, and hand the request to `env.API_WORKER`.
|
|
14
|
+
* - **Mail-Meow** — the hop-by-hop header strip, and the decision to *always*
|
|
15
|
+
* delete `X-Forwarded-For` before conditionally setting it from
|
|
16
|
+
* `CF-Connecting-IP`. Setting it only when the header was present left a
|
|
17
|
+
* caller-supplied value intact on any request that reached Pages without
|
|
18
|
+
* `CF-Connecting-IP`, so the origin would trust an attacker-chosen client IP.
|
|
19
|
+
* - **Durable-DAV-Router** — `duplex: 'half'`, required whenever the forwarded
|
|
20
|
+
* body is a stream. Without it this threw a `TypeError` on every non-GET
|
|
21
|
+
* request rather than proxying it.
|
|
22
|
+
* - **Edge-Sonic** — the client-forwarding-header strip list, which additionally
|
|
23
|
+
* removes `Forwarded`, `True-Client-IP` and `X-Real-Ip`. Edge-Sonic goes
|
|
24
|
+
* further and *never* sets `X-Forwarded-For` at all, because its worker keys
|
|
25
|
+
* the rate limiter on `CF-Connecting-IP` and refuses `X-Forwarded-For` on
|
|
26
|
+
* purpose.
|
|
27
|
+
*
|
|
28
|
+
* ### The convergence decision on `X-Forwarded-For`
|
|
29
|
+
*
|
|
30
|
+
* The two families disagree, and the disagreement is about which worker is
|
|
31
|
+
* behind the binding. Mail-Meow's behaviour is kept as the default because it is
|
|
32
|
+
* the only one that preserves the client IP for the audit trail, which five repos
|
|
33
|
+
* read. A repo whose worker refuses the header — Edge-Sonic's — passes
|
|
34
|
+
* `forwardClientIp: false`, which deletes it and sets nothing. What is *not*
|
|
35
|
+
* available is the source repos' conditional `if (clientIp) set(...)`: that is
|
|
36
|
+
* the defect Mail-Meow recorded fixing, and a factory that could reintroduce it
|
|
37
|
+
* would be a factory that carries the bug forward.
|
|
38
|
+
*
|
|
39
|
+
* ### `duplex: 'half'` rides with the body
|
|
40
|
+
*
|
|
41
|
+
* Required by the Fetch spec whenever a `Request` is constructed with a **stream**
|
|
42
|
+
* body. workerd does not throw without it, which is why the line was absent from
|
|
43
|
+
* every Pages copy — and Node does, which is why `test/pages-proxy.test.ts` in
|
|
44
|
+
* Edge-Sonic could not drive the handler at all. It is spread only where a body
|
|
45
|
+
* is forwarded, because a `RequestInit` carrying `duplex` and no body is a shape
|
|
46
|
+
* no runtime documents; the cast is `as RequestInit` either way, since `duplex`
|
|
47
|
+
* is not in the DOM lib's type.
|
|
48
|
+
*
|
|
49
|
+
* The result is a handler factory, so a consumer's `functions/[[path]].ts` is a
|
|
50
|
+
* one-line re-export. Structural `Fetcher` and `PagesFunction` types are declared
|
|
51
|
+
* here rather than imported from `@cloudflare/workers-types`, so the kit stays
|
|
52
|
+
* zero-dependency and this module typechecks without the generated
|
|
53
|
+
* `worker-configuration.d.ts`.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* The subset of Cloudflare's `Fetcher` this proxy uses.
|
|
57
|
+
*
|
|
58
|
+
* Structural, not imported: the shim only ever calls `fetch`, and requiring
|
|
59
|
+
* `@cloudflare/workers-types` for one method would couple the kit to a generated
|
|
60
|
+
* types file that only exists inside a Wrangler project.
|
|
61
|
+
*/
|
|
62
|
+
export interface Fetcher {
|
|
63
|
+
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The environment a Pages Function is invoked with.
|
|
67
|
+
*/
|
|
68
|
+
export interface PagesProxyEnv {
|
|
69
|
+
readonly API_WORKER: Fetcher;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The context Cloudflare passes to `onRequest`.
|
|
73
|
+
*
|
|
74
|
+
* Only the fields this proxy reads are declared. `params` and `waitUntil` are
|
|
75
|
+
* omitted because the catch-all does not use them — a shim that named them would
|
|
76
|
+
* be claiming a contract it does not exercise.
|
|
77
|
+
*/
|
|
78
|
+
export interface PagesFunctionContext {
|
|
79
|
+
readonly request: Request;
|
|
80
|
+
readonly env: PagesProxyEnv;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A Pages Function handler.
|
|
84
|
+
*/
|
|
85
|
+
export type PagesFunction = (context: PagesFunctionContext) => Promise<Response>;
|
|
86
|
+
/**
|
|
87
|
+
* What `createPagesProxy` accepts.
|
|
88
|
+
*/
|
|
89
|
+
export interface PagesProxyOptions {
|
|
90
|
+
/**
|
|
91
|
+
* Set `X-Forwarded-For` from `CF-Connecting-IP`.
|
|
92
|
+
*
|
|
93
|
+
* Default `true` — Mail-Meow's behaviour, which five repos rely on for their
|
|
94
|
+
* audit trail. Pass `false` for a worker that refuses the header outright
|
|
95
|
+
* (Edge-Sonic's does), and the header is deleted and nothing set.
|
|
96
|
+
*/
|
|
97
|
+
readonly forwardClientIp?: boolean;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The `onRequest` handler for `functions/[[path]].ts`.
|
|
101
|
+
*
|
|
102
|
+
* Usage — the consumer's `functions/[[path]].ts` becomes two lines:
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { createPagesProxy } from '@rexezuge/tooling/functions/pages-proxy';
|
|
106
|
+
* export const onRequest = createPagesProxy();
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @param options see {@link PagesProxyOptions}. Omitted gives the family's
|
|
110
|
+
* majority behaviour.
|
|
111
|
+
*/
|
|
112
|
+
export declare function createPagesProxy(options?: PagesProxyOptions): PagesFunction;
|
|
113
|
+
//# sourceMappingURL=pages-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages-proxy.d.ts","sourceRoot":"","sources":["../../src/functions/pages-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC7E;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAgCjF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa,CAuC/E"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cloudflare Pages catch-all proxy.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: the `functions/[[path]].ts` shim duplicated in Durable-DAV,
|
|
5
|
+
* Mail-Meow and Edge-Git (and present in nine repos in total). Each copy is an
|
|
6
|
+
* `onRequest` handler that forwards the request to the API worker through a
|
|
7
|
+
* service binding, rewriting the three headers the worker routes on.
|
|
8
|
+
*
|
|
9
|
+
* What each source contributes:
|
|
10
|
+
*
|
|
11
|
+
* - **Durable-DAV / Edge-Git** — the core shape: set `X-Forwarded-Host`,
|
|
12
|
+
* `X-Forwarded-Proto` and `X-Forwarded-Uri` from the original URL, forward the
|
|
13
|
+
* body for non-GET/HEAD methods, and hand the request to `env.API_WORKER`.
|
|
14
|
+
* - **Mail-Meow** — the hop-by-hop header strip, and the decision to *always*
|
|
15
|
+
* delete `X-Forwarded-For` before conditionally setting it from
|
|
16
|
+
* `CF-Connecting-IP`. Setting it only when the header was present left a
|
|
17
|
+
* caller-supplied value intact on any request that reached Pages without
|
|
18
|
+
* `CF-Connecting-IP`, so the origin would trust an attacker-chosen client IP.
|
|
19
|
+
* - **Durable-DAV-Router** — `duplex: 'half'`, required whenever the forwarded
|
|
20
|
+
* body is a stream. Without it this threw a `TypeError` on every non-GET
|
|
21
|
+
* request rather than proxying it.
|
|
22
|
+
* - **Edge-Sonic** — the client-forwarding-header strip list, which additionally
|
|
23
|
+
* removes `Forwarded`, `True-Client-IP` and `X-Real-Ip`. Edge-Sonic goes
|
|
24
|
+
* further and *never* sets `X-Forwarded-For` at all, because its worker keys
|
|
25
|
+
* the rate limiter on `CF-Connecting-IP` and refuses `X-Forwarded-For` on
|
|
26
|
+
* purpose.
|
|
27
|
+
*
|
|
28
|
+
* ### The convergence decision on `X-Forwarded-For`
|
|
29
|
+
*
|
|
30
|
+
* The two families disagree, and the disagreement is about which worker is
|
|
31
|
+
* behind the binding. Mail-Meow's behaviour is kept as the default because it is
|
|
32
|
+
* the only one that preserves the client IP for the audit trail, which five repos
|
|
33
|
+
* read. A repo whose worker refuses the header — Edge-Sonic's — passes
|
|
34
|
+
* `forwardClientIp: false`, which deletes it and sets nothing. What is *not*
|
|
35
|
+
* available is the source repos' conditional `if (clientIp) set(...)`: that is
|
|
36
|
+
* the defect Mail-Meow recorded fixing, and a factory that could reintroduce it
|
|
37
|
+
* would be a factory that carries the bug forward.
|
|
38
|
+
*
|
|
39
|
+
* ### `duplex: 'half'` rides with the body
|
|
40
|
+
*
|
|
41
|
+
* Required by the Fetch spec whenever a `Request` is constructed with a **stream**
|
|
42
|
+
* body. workerd does not throw without it, which is why the line was absent from
|
|
43
|
+
* every Pages copy — and Node does, which is why `test/pages-proxy.test.ts` in
|
|
44
|
+
* Edge-Sonic could not drive the handler at all. It is spread only where a body
|
|
45
|
+
* is forwarded, because a `RequestInit` carrying `duplex` and no body is a shape
|
|
46
|
+
* no runtime documents; the cast is `as RequestInit` either way, since `duplex`
|
|
47
|
+
* is not in the DOM lib's type.
|
|
48
|
+
*
|
|
49
|
+
* The result is a handler factory, so a consumer's `functions/[[path]].ts` is a
|
|
50
|
+
* one-line re-export. Structural `Fetcher` and `PagesFunction` types are declared
|
|
51
|
+
* here rather than imported from `@cloudflare/workers-types`, so the kit stays
|
|
52
|
+
* zero-dependency and this module typechecks without the generated
|
|
53
|
+
* `worker-configuration.d.ts`.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Headers a *client* may have set, stripped before the request crosses the
|
|
57
|
+
* binding.
|
|
58
|
+
*
|
|
59
|
+
* These describe the connection between the client and the proxy. Forwarding
|
|
60
|
+
* them would let the origin believe the proxy's view of the client is the
|
|
61
|
+
* client's own claim about itself. The three the proxy sets itself are set from
|
|
62
|
+
* values it reads off the request, not from the client's headers.
|
|
63
|
+
*/
|
|
64
|
+
const CLIENT_FORWARDING_HEADERS = [
|
|
65
|
+
'connection',
|
|
66
|
+
'keep-alive',
|
|
67
|
+
'proxy-authenticate',
|
|
68
|
+
'proxy-authorization',
|
|
69
|
+
'te',
|
|
70
|
+
'trailer',
|
|
71
|
+
'transfer-encoding',
|
|
72
|
+
'upgrade',
|
|
73
|
+
'X-Forwarded-For',
|
|
74
|
+
'X-Forwarded-Host',
|
|
75
|
+
'X-Forwarded-Proto',
|
|
76
|
+
'X-Forwarded-Uri',
|
|
77
|
+
'Forwarded',
|
|
78
|
+
'True-Client-IP',
|
|
79
|
+
'X-Real-Ip',
|
|
80
|
+
];
|
|
81
|
+
/** The header Cloudflare sets with the socket's peer address. */
|
|
82
|
+
const CF_CONNECTING_IP_HEADER = 'CF-Connecting-IP';
|
|
83
|
+
/**
|
|
84
|
+
* The `onRequest` handler for `functions/[[path]].ts`.
|
|
85
|
+
*
|
|
86
|
+
* Usage — the consumer's `functions/[[path]].ts` becomes two lines:
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { createPagesProxy } from '@rexezuge/tooling/functions/pages-proxy';
|
|
90
|
+
* export const onRequest = createPagesProxy();
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @param options see {@link PagesProxyOptions}. Omitted gives the family's
|
|
94
|
+
* majority behaviour.
|
|
95
|
+
*/
|
|
96
|
+
export function createPagesProxy(options = {}) {
|
|
97
|
+
const forwardClientIp = options.forwardClientIp ?? true;
|
|
98
|
+
return async ({ request, env }) => {
|
|
99
|
+
const originalUrl = new URL(request.url);
|
|
100
|
+
const headers = new Headers(request.headers);
|
|
101
|
+
for (const header of CLIENT_FORWARDING_HEADERS)
|
|
102
|
+
headers.delete(header);
|
|
103
|
+
// Set from values read off the request rather than from the client: only the
|
|
104
|
+
// proxy knows which host it served, and a service binding hands over a
|
|
105
|
+
// rewritten URL, so the original path is what has to be carried across.
|
|
106
|
+
headers.set('X-Forwarded-Host', originalUrl.host);
|
|
107
|
+
headers.set('X-Forwarded-Proto', originalUrl.protocol.replace(':', ''));
|
|
108
|
+
headers.set('X-Forwarded-Uri', `${originalUrl.pathname}${originalUrl.search}`);
|
|
109
|
+
if (forwardClientIp) {
|
|
110
|
+
const clientIp = request.headers.get(CF_CONNECTING_IP_HEADER);
|
|
111
|
+
if (clientIp)
|
|
112
|
+
headers.set('X-Forwarded-For', clientIp);
|
|
113
|
+
}
|
|
114
|
+
// `GET`/`HEAD` carry no body, and a `Request` with one is rejected outright
|
|
115
|
+
// for those methods. The body is forwarded as a *stream*, so a large upload
|
|
116
|
+
// does not have to fit in memory on the Pages side.
|
|
117
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
118
|
+
const proxyRequest = new Request(originalUrl.href, {
|
|
119
|
+
method: request.method,
|
|
120
|
+
headers,
|
|
121
|
+
body: hasBody ? request.body : undefined,
|
|
122
|
+
// See the header: `duplex` is not in the DOM lib's `RequestInit` type, and
|
|
123
|
+
// removing the cast would mean not forwarding bodies at all, which is a
|
|
124
|
+
// worse answer than one assertion.
|
|
125
|
+
...(hasBody && { duplex: 'half' }),
|
|
126
|
+
redirect: request.redirect,
|
|
127
|
+
});
|
|
128
|
+
return env.API_WORKER.fetch(proxyRequest);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=pages-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages-proxy.js","sourceRoot":"","sources":["../../src/functions/pages-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAqCH;;;;;;;;GAQG;AACH,MAAM,yBAAyB,GAAsB;IACnD,YAAY;IACZ,YAAY;IACZ,oBAAoB;IACpB,qBAAqB;IACrB,IAAI;IACJ,SAAS;IACT,mBAAmB;IACnB,SAAS;IACT,iBAAiB;IACjB,kBAAkB;IAClB,mBAAmB;IACnB,iBAAiB;IACjB,WAAW;IACX,gBAAgB;IAChB,WAAW;CACZ,CAAC;AAEF,iEAAiE;AACjE,MAAM,uBAAuB,GAAG,kBAAkB,CAAC;AAgBnD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAA6B,EAAE;IAC9D,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,IAAI,CAAC;IAExD,OAAO,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,EAAwB,EAAqB,EAAE;QACzE,MAAM,WAAW,GAAQ,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE9C,MAAM,OAAO,GAAY,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACtD,KAAK,MAAM,MAAM,IAAI,yBAAyB;YAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAEvE,6EAA6E;QAC7E,uEAAuE;QACvE,wEAAwE;QACxE,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;QACxE,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,GAAG,WAAW,CAAC,QAAQ,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;QAE/E,IAAI,eAAe,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAkB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;YAC7E,IAAI,QAAQ;gBAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;QACzD,CAAC;QAED,4EAA4E;QAC5E,4EAA4E;QAC5E,oDAAoD;QACpD,MAAM,OAAO,GAAY,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC;QAE/E,MAAM,YAAY,GAAY,IAAI,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE;YAC1D,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO;YACP,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YACxC,2EAA2E;YAC3E,wEAAwE;YACxE,mCAAmC;YACnC,GAAG,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;YAClC,QAAQ,EAAE,OAAO,CAAC,QAAQ;SACZ,CAAC,CAAC;QAElB,OAAO,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC5C,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rexezuge/tooling` — the tooling layer of the kit.
|
|
3
|
+
*
|
|
4
|
+
* The config factories and the test helpers are re-exported here, so a consumer has
|
|
5
|
+
* one import for everything its `*.config.mts` files and its mocks need. The
|
|
6
|
+
* scripts under `scripts/` are **not** re-exported: they resolve paths relative to
|
|
7
|
+
* the repository they run in, so they are run from disk with `tsx`, not imported.
|
|
8
|
+
* See `README.md` for the copy-in recipe and the per-script usage.
|
|
9
|
+
*
|
|
10
|
+
* The zero-dependency rule: nothing here imports another `@rexezuge/*` package. The
|
|
11
|
+
* one exception in the tree is `scripts/backup/encrypt-backup.ts`, which uses
|
|
12
|
+
* `@rexezuge/d1`'s AES-GCM so the backup and the workers share one definition of
|
|
13
|
+
* "what an encryption key looks like"; that dependency is declared on the package
|
|
14
|
+
* and is only ever resolved in the consumer's repo, where the kit is installed
|
|
15
|
+
* alongside `@rexezuge/d1`.
|
|
16
|
+
*/
|
|
17
|
+
export { defineRepoLintConfig } from './eslint';
|
|
18
|
+
export type { RepoLintConfigOptions } from './eslint';
|
|
19
|
+
export { defineUnitVitestConfig } from './vitest';
|
|
20
|
+
export type { CoverageThresholds, UnitVitestConfigOptions, VitestAlias } from './vitest';
|
|
21
|
+
export { defineWebVitestConfig } from './vitest-web';
|
|
22
|
+
export type { WebVitestConfigOptions } from './vitest-web';
|
|
23
|
+
export { defineWebViteConfig, spaShellEmbedPlugin } from './vite';
|
|
24
|
+
export type { ProxyEntry, SpaShellTarget, WebViteConfigOptions } from './vite';
|
|
25
|
+
export { createPagesProxy } from './functions/pages-proxy';
|
|
26
|
+
export type { Fetcher, PagesFunction, PagesFunctionContext, PagesProxyEnv, PagesProxyOptions } from './functions/pages-proxy';
|
|
27
|
+
export { DurableObject, RpcTarget, WorkflowEntrypoint } from './test/mocks/cloudflare-workers';
|
|
28
|
+
export type { DurableObjectState, ExecutionContext } from './test/mocks/cloudflare-workers';
|
|
29
|
+
export { createMigrationHelper, readMigrationFiles, splitSql } from './test/integration-migrations';
|
|
30
|
+
export type { D1DatabaseLike, D1PreparedStatementLike, MigrationFile, MigrationHelper, MigrationHelperOptions, MigrationRange } from './test/integration-migrations';
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAClE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAO9H,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAE5F,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACpG,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,aAAa,EAAE,eAAe,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rexezuge/tooling` — the tooling layer of the kit.
|
|
3
|
+
*
|
|
4
|
+
* The config factories and the test helpers are re-exported here, so a consumer has
|
|
5
|
+
* one import for everything its `*.config.mts` files and its mocks need. The
|
|
6
|
+
* scripts under `scripts/` are **not** re-exported: they resolve paths relative to
|
|
7
|
+
* the repository they run in, so they are run from disk with `tsx`, not imported.
|
|
8
|
+
* See `README.md` for the copy-in recipe and the per-script usage.
|
|
9
|
+
*
|
|
10
|
+
* The zero-dependency rule: nothing here imports another `@rexezuge/*` package. The
|
|
11
|
+
* one exception in the tree is `scripts/backup/encrypt-backup.ts`, which uses
|
|
12
|
+
* `@rexezuge/d1`'s AES-GCM so the backup and the workers share one definition of
|
|
13
|
+
* "what an encryption key looks like"; that dependency is declared on the package
|
|
14
|
+
* and is only ever resolved in the consumer's repo, where the kit is installed
|
|
15
|
+
* alongside `@rexezuge/d1`.
|
|
16
|
+
*/
|
|
17
|
+
export { defineRepoLintConfig } from './eslint';
|
|
18
|
+
export { defineUnitVitestConfig } from './vitest';
|
|
19
|
+
export { defineWebVitestConfig } from './vitest-web';
|
|
20
|
+
export { defineWebViteConfig, spaShellEmbedPlugin } from './vite';
|
|
21
|
+
export { createPagesProxy } from './functions/pages-proxy';
|
|
22
|
+
// --- Test helpers ---------------------------------------------------------
|
|
23
|
+
//
|
|
24
|
+
// Aliased in a Vitest config, not imported by application code, so the mocks stay
|
|
25
|
+
// where the suites can reach them.
|
|
26
|
+
export { DurableObject, RpcTarget, WorkflowEntrypoint } from './test/mocks/cloudflare-workers';
|
|
27
|
+
export { createMigrationHelper, readMigrationFiles, splitSql } from './test/integration-migrations';
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAGhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGlD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAGlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,6EAA6E;AAC7E,EAAE;AACF,kFAAkF;AAClF,mCAAmC;AAEnC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAG/F,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing which D1 database the backup workflow exports.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: ported from Edge-Sonic's `scripts/backup/d1-target.ts`. Pure, so the
|
|
5
|
+
* empty-database guard is asserted without running Actions or touching a real
|
|
6
|
+
* account.
|
|
7
|
+
*/
|
|
8
|
+
/** A D1 binding as a parsed wrangler config carries one. */
|
|
9
|
+
export interface D1DatabaseBinding {
|
|
10
|
+
binding?: string;
|
|
11
|
+
database_id?: string;
|
|
12
|
+
database_name?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The resolved export target. */
|
|
15
|
+
export interface D1Target {
|
|
16
|
+
/** The binding name, used for log output and as the export fallback. */
|
|
17
|
+
binding: string;
|
|
18
|
+
/** The database id, verified against Cloudflare by the caller. */
|
|
19
|
+
databaseId: string;
|
|
20
|
+
/** What `wrangler d1 export` should be pointed at. */
|
|
21
|
+
target: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Pick the export target from a parsed wrangler config.
|
|
25
|
+
*
|
|
26
|
+
* `wrangler d1 export` takes a database *name*, so `database_name` wins when
|
|
27
|
+
* present. Hand-written configs that omit it fall back to the binding name.
|
|
28
|
+
*
|
|
29
|
+
* @throws when there is no binding, or when `database_id` is still the template
|
|
30
|
+
* placeholder — meaning the database was auto-provisioned and holds no data.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveD1Target(config: {
|
|
33
|
+
d1_databases?: D1DatabaseBinding[];
|
|
34
|
+
}): D1Target;
|
|
35
|
+
//# sourceMappingURL=d1-target.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"d1-target.d.ts","sourceRoot":"","sources":["../../../src/scripts/backup/d1-target.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,kCAAkC;AAClC,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IAAE,YAAY,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAAE,GAAG,QAAQ,CAiBxF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing which D1 database the backup workflow exports.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: ported from Edge-Sonic's `scripts/backup/d1-target.ts`. Pure, so the
|
|
5
|
+
* empty-database guard is asserted without running Actions or touching a real
|
|
6
|
+
* account.
|
|
7
|
+
*/
|
|
8
|
+
import { DEFAULT_UUID } from '../wrangler-config/types';
|
|
9
|
+
/**
|
|
10
|
+
* Pick the export target from a parsed wrangler config.
|
|
11
|
+
*
|
|
12
|
+
* `wrangler d1 export` takes a database *name*, so `database_name` wins when
|
|
13
|
+
* present. Hand-written configs that omit it fall back to the binding name.
|
|
14
|
+
*
|
|
15
|
+
* @throws when there is no binding, or when `database_id` is still the template
|
|
16
|
+
* placeholder — meaning the database was auto-provisioned and holds no data.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveD1Target(config) {
|
|
19
|
+
const database = config.d1_databases?.[0];
|
|
20
|
+
const binding = database?.binding?.trim();
|
|
21
|
+
if (binding === undefined || binding === '') {
|
|
22
|
+
throw new Error('No d1_databases entry with a binding found in wrangler.jsonc.');
|
|
23
|
+
}
|
|
24
|
+
const databaseId = database?.database_id?.trim() ?? '';
|
|
25
|
+
if (databaseId === DEFAULT_UUID) {
|
|
26
|
+
throw new Error(`D1 database_id for ${binding} is still the template placeholder. The database does not exist yet, so there is nothing to back up.`);
|
|
27
|
+
}
|
|
28
|
+
const target = database?.database_name?.trim() || binding;
|
|
29
|
+
return { binding, databaseId, target };
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=d1-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"d1-target.js","sourceRoot":"","sources":["../../../src/scripts/backup/d1-target.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAmBxD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,MAA8C;IAC5E,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAG,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC1C,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,UAAU,GAAG,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACvD,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,sBAAsB,OAAO,sGAAsG,CACpI,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC;IAC1D,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destination and encryption policy for the D1 backup workflow.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: ported from Edge-Sonic's `scripts/backup/destination-config.ts`.
|
|
5
|
+
* Pure and side-effect free, so the fail-closed rules are asserted without running
|
|
6
|
+
* Actions.
|
|
7
|
+
*/
|
|
8
|
+
/** The environment the `check-secrets` job is given, as repository secrets. */
|
|
9
|
+
export interface BackupSecretEnvironment {
|
|
10
|
+
CLOUDFLARE_API_TOKEN?: string;
|
|
11
|
+
CLOUDFLARE_ACCOUNT_ID?: string;
|
|
12
|
+
BACKUP_ENCRYPTION_KEY?: string;
|
|
13
|
+
S3_ACCESS_KEY_ID?: string;
|
|
14
|
+
S3_SECRET_ACCESS_KEY?: string;
|
|
15
|
+
S3_BUCKET?: string;
|
|
16
|
+
WEBDAV_URL?: string;
|
|
17
|
+
WEBDAV_USER?: string;
|
|
18
|
+
WEBDAV_PASSWORD?: string;
|
|
19
|
+
}
|
|
20
|
+
/** What is configured, and why the run cannot proceed when it is not. */
|
|
21
|
+
export interface DestinationConfig {
|
|
22
|
+
/** Cloudflare credentials present, so the export job can run. */
|
|
23
|
+
cloudflare: boolean;
|
|
24
|
+
/** An encryption passphrase is available. */
|
|
25
|
+
encryption: boolean;
|
|
26
|
+
/** At least one upload destination is configured. */
|
|
27
|
+
anyDestination: boolean;
|
|
28
|
+
/** The S3 destination is fully configured. */
|
|
29
|
+
s3: boolean;
|
|
30
|
+
/** The WebDAV destination is fully configured. */
|
|
31
|
+
webdav: boolean;
|
|
32
|
+
/** Human-readable reasons the run cannot proceed, empty when it can. */
|
|
33
|
+
errors: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decides which destinations are configured and enforces fail-closed encryption.
|
|
37
|
+
*
|
|
38
|
+
* ### Why the encryption requirement is not optional here
|
|
39
|
+
*
|
|
40
|
+
* Two independent reasons, and they have different consequences so both are stated.
|
|
41
|
+
*
|
|
42
|
+
* **The at-rest protection assumes the dump is not in hand.** Every repo in this
|
|
43
|
+
* family seals credentials with AES-256-GCM, and each one's migration notes say the
|
|
44
|
+
* same thing: *obfuscation against a D1 dump, not protection against a full worker
|
|
45
|
+
* compromise.* An unencrypted backup inverts that assumption — dump plus the
|
|
46
|
+
* Secrets Store key is a complete credential set, and the WebDAV key is read on
|
|
47
|
+
* every scan and every stream.
|
|
48
|
+
*
|
|
49
|
+
* **The dump is a listening history.** Stars, ratings, play counts, play queues and
|
|
50
|
+
* auth failures record what each user does and when, and the schema records the
|
|
51
|
+
* full topology and every file path. None of that is credential material, and all
|
|
52
|
+
* of it is nobody else's to read.
|
|
53
|
+
*
|
|
54
|
+
* `S3_REGION` is deliberately not part of the S3 gate. R2 and MinIO have no
|
|
55
|
+
* meaningful region — Cloudflare documents an R2 bucket's region as `auto`, with
|
|
56
|
+
* empty and `us-east-1` both aliasing to it — so requiring the secret would
|
|
57
|
+
* silently disable backups for anyone using such a provider. The upload script
|
|
58
|
+
* defaults `AWS_DEFAULT_REGION` to `auto` instead.
|
|
59
|
+
*/
|
|
60
|
+
export declare function evaluateConfig(env: BackupSecretEnvironment): DestinationConfig;
|
|
61
|
+
//# sourceMappingURL=destination-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destination-config.d.ts","sourceRoot":"","sources":["../../../src/scripts/backup/destination-config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,uBAAuB;IACtC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,UAAU,EAAE,OAAO,CAAC;IACpB,6CAA6C;IAC7C,UAAU,EAAE,OAAO,CAAC;IACpB,qDAAqD;IACrD,cAAc,EAAE,OAAO,CAAC;IACxB,8CAA8C;IAC9C,EAAE,EAAE,OAAO,CAAC;IACZ,kDAAkD;IAClD,MAAM,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAOD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,uBAAuB,GAAG,iBAAiB,CAwB9E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destination and encryption policy for the D1 backup workflow.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: ported from Edge-Sonic's `scripts/backup/destination-config.ts`.
|
|
5
|
+
* Pure and side-effect free, so the fail-closed rules are asserted without running
|
|
6
|
+
* Actions.
|
|
7
|
+
*/
|
|
8
|
+
/** A secret counts as set only when it has non-whitespace content. */
|
|
9
|
+
function isSet(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Decides which destinations are configured and enforces fail-closed encryption.
|
|
14
|
+
*
|
|
15
|
+
* ### Why the encryption requirement is not optional here
|
|
16
|
+
*
|
|
17
|
+
* Two independent reasons, and they have different consequences so both are stated.
|
|
18
|
+
*
|
|
19
|
+
* **The at-rest protection assumes the dump is not in hand.** Every repo in this
|
|
20
|
+
* family seals credentials with AES-256-GCM, and each one's migration notes say the
|
|
21
|
+
* same thing: *obfuscation against a D1 dump, not protection against a full worker
|
|
22
|
+
* compromise.* An unencrypted backup inverts that assumption — dump plus the
|
|
23
|
+
* Secrets Store key is a complete credential set, and the WebDAV key is read on
|
|
24
|
+
* every scan and every stream.
|
|
25
|
+
*
|
|
26
|
+
* **The dump is a listening history.** Stars, ratings, play counts, play queues and
|
|
27
|
+
* auth failures record what each user does and when, and the schema records the
|
|
28
|
+
* full topology and every file path. None of that is credential material, and all
|
|
29
|
+
* of it is nobody else's to read.
|
|
30
|
+
*
|
|
31
|
+
* `S3_REGION` is deliberately not part of the S3 gate. R2 and MinIO have no
|
|
32
|
+
* meaningful region — Cloudflare documents an R2 bucket's region as `auto`, with
|
|
33
|
+
* empty and `us-east-1` both aliasing to it — so requiring the secret would
|
|
34
|
+
* silently disable backups for anyone using such a provider. The upload script
|
|
35
|
+
* defaults `AWS_DEFAULT_REGION` to `auto` instead.
|
|
36
|
+
*/
|
|
37
|
+
export function evaluateConfig(env) {
|
|
38
|
+
const cloudflare = isSet(env.CLOUDFLARE_API_TOKEN) && isSet(env.CLOUDFLARE_ACCOUNT_ID);
|
|
39
|
+
const encryption = isSet(env.BACKUP_ENCRYPTION_KEY);
|
|
40
|
+
const s3 = isSet(env.S3_ACCESS_KEY_ID) && isSet(env.S3_SECRET_ACCESS_KEY) && isSet(env.S3_BUCKET);
|
|
41
|
+
const webdav = isSet(env.WEBDAV_URL) && isSet(env.WEBDAV_USER) && isSet(env.WEBDAV_PASSWORD);
|
|
42
|
+
const anyDestination = s3 || webdav;
|
|
43
|
+
const errors = [];
|
|
44
|
+
// Fail closed, but only once someone has opted into a destination. An
|
|
45
|
+
// unconfigured repository is a legitimate "I do not want off-site backups" state
|
|
46
|
+
// and must not turn the schedule red.
|
|
47
|
+
if (anyDestination) {
|
|
48
|
+
if (!cloudflare) {
|
|
49
|
+
errors.push('A backup destination is configured but CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID are missing.');
|
|
50
|
+
}
|
|
51
|
+
if (!encryption) {
|
|
52
|
+
errors.push('BACKUP_ENCRYPTION_KEY is required. The D1 dump holds AES-GCM-obfuscated credentials — obfuscation that, by its own design, assumes the dump is not in hand — plus every user history and the full schema topology.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { cloudflare, encryption, anyDestination, s3, webdav, errors };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=destination-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destination-config.js","sourceRoot":"","sources":["../../../src/scripts/backup/destination-config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+BH,sEAAsE;AACtE,SAAS,KAAK,CAAC,KAAyB;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,cAAc,CAAC,GAA4B;IACzD,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACvF,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACpD,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAClG,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC7F,MAAM,cAAc,GAAG,EAAE,IAAI,MAAM,CAAC;IAEpC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,sEAAsE;IACtE,iFAAiF;IACjF,sCAAsC;IACtC,IAAI,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,kGAAkG,CAAC,CAAC;QAClH,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CACT,oNAAoN,CACrN,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Compress and encrypt the exported D1 dump.
|
|
4
|
+
*
|
|
5
|
+
* Provenance: Edge-Sonic's `scripts/backup/encrypt-backup.ts`, converged with the
|
|
6
|
+
* AES-GCM envelope every repo in the family already uses for its secrets. This is
|
|
7
|
+
* the one backup script that ported cleanly and changed *for the better* on the way
|
|
8
|
+
* in: the source shelled out to `openssl enc -aes-256-cbc -pbkdf2`, which is a
|
|
9
|
+
* password-derived key with a KDF whose cost is a constant somebody chose once,
|
|
10
|
+
* and which is not authenticated — a flipped byte in the ciphertext is undetectable.
|
|
11
|
+
*
|
|
12
|
+
* The kit's version uses `@rexezuge/d1`'s `encryptSecret`, the same WebCrypto
|
|
13
|
+
* AES-256-GCM primitive the workers use, over a base64 32-byte key — the same key
|
|
14
|
+
* format `resolveReplicationKey` reads. One definition of "what an encryption key
|
|
15
|
+
* looks like" across the deploy path and the backup path is the point: a second
|
|
16
|
+
* implementation is how a deploy ends up generating something the worker cannot
|
|
17
|
+
* read, and here it would be how a backup ends up unreadable by the restore path.
|
|
18
|
+
*
|
|
19
|
+
* Fail-closed: without `BACKUP_ENCRYPTION_KEY` the script refuses to run and
|
|
20
|
+
* deletes nothing, so an unencrypted dump can never reach the artifact store the
|
|
21
|
+
* upload jobs read from.
|
|
22
|
+
*
|
|
23
|
+
* The passphrase is read from the environment rather than passed as an argument,
|
|
24
|
+
* because command-line arguments are visible to any process on the runner via `ps`.
|
|
25
|
+
*
|
|
26
|
+
* Emits the resulting file name as the `file` step output.
|
|
27
|
+
*
|
|
28
|
+
* Usage, from the repo root (after `wrangler d1 export … --output=backup.sql`):
|
|
29
|
+
*
|
|
30
|
+
* ```bash
|
|
31
|
+
* BACKUP_ENCRYPTION_KEY=… pnpm exec tsx scripts/backup/encrypt-backup.ts
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=encrypt-backup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encrypt-backup.d.ts","sourceRoot":"","sources":["../../../src/scripts/backup/encrypt-backup.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG"}
|