@rexezuge/tooling 0.0.0-stage → 1.0.1

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +297 -2
  3. package/dist/eslint-boundaries.d.ts +75 -0
  4. package/dist/eslint-boundaries.d.ts.map +1 -0
  5. package/dist/eslint-boundaries.js +138 -0
  6. package/dist/eslint-boundaries.js.map +1 -0
  7. package/dist/eslint-rules.d.ts +41 -0
  8. package/dist/eslint-rules.d.ts.map +1 -0
  9. package/dist/eslint-rules.js +273 -0
  10. package/dist/eslint-rules.js.map +1 -0
  11. package/dist/eslint.d.ts +96 -0
  12. package/dist/eslint.d.ts.map +1 -0
  13. package/dist/eslint.js +189 -0
  14. package/dist/eslint.js.map +1 -0
  15. package/dist/functions/pages-proxy.d.ts +113 -0
  16. package/dist/functions/pages-proxy.d.ts.map +1 -0
  17. package/dist/functions/pages-proxy.js +131 -0
  18. package/dist/functions/pages-proxy.js.map +1 -0
  19. package/dist/index.d.ts +31 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +28 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/scripts/backup/d1-target.d.ts +35 -0
  24. package/dist/scripts/backup/d1-target.d.ts.map +1 -0
  25. package/dist/scripts/backup/d1-target.js +31 -0
  26. package/dist/scripts/backup/d1-target.js.map +1 -0
  27. package/dist/scripts/backup/destination-config.d.ts +61 -0
  28. package/dist/scripts/backup/destination-config.d.ts.map +1 -0
  29. package/dist/scripts/backup/destination-config.js +57 -0
  30. package/dist/scripts/backup/destination-config.js.map +1 -0
  31. package/dist/scripts/backup/encrypt-backup.d.ts +35 -0
  32. package/dist/scripts/backup/encrypt-backup.d.ts.map +1 -0
  33. package/dist/scripts/backup/encrypt-backup.js +98 -0
  34. package/dist/scripts/backup/encrypt-backup.js.map +1 -0
  35. package/dist/scripts/backup/naming.d.ts +44 -0
  36. package/dist/scripts/backup/naming.d.ts.map +1 -0
  37. package/dist/scripts/backup/naming.js +58 -0
  38. package/dist/scripts/backup/naming.js.map +1 -0
  39. package/dist/scripts/check-god-files.d.ts +164 -0
  40. package/dist/scripts/check-god-files.d.ts.map +1 -0
  41. package/dist/scripts/check-god-files.js +271 -0
  42. package/dist/scripts/check-god-files.js.map +1 -0
  43. package/dist/scripts/ensure-spa-shell-stub.d.ts +3 -0
  44. package/dist/scripts/ensure-spa-shell-stub.d.ts.map +1 -0
  45. package/dist/scripts/ensure-spa-shell-stub.js +30 -0
  46. package/dist/scripts/ensure-spa-shell-stub.js.map +1 -0
  47. package/dist/scripts/init-secrets.d.ts +63 -0
  48. package/dist/scripts/init-secrets.d.ts.map +1 -0
  49. package/dist/scripts/init-secrets.js +240 -0
  50. package/dist/scripts/init-secrets.js.map +1 -0
  51. package/dist/scripts/lib/cli-args.d.ts +78 -0
  52. package/dist/scripts/lib/cli-args.d.ts.map +1 -0
  53. package/dist/scripts/lib/cli-args.js +116 -0
  54. package/dist/scripts/lib/cli-args.js.map +1 -0
  55. package/dist/scripts/lib/github-actions.d.ts +26 -0
  56. package/dist/scripts/lib/github-actions.d.ts.map +1 -0
  57. package/dist/scripts/lib/github-actions.js +38 -0
  58. package/dist/scripts/lib/github-actions.js.map +1 -0
  59. package/dist/scripts/lib/wrangler-table.d.ts +46 -0
  60. package/dist/scripts/lib/wrangler-table.d.ts.map +1 -0
  61. package/dist/scripts/lib/wrangler-table.js +99 -0
  62. package/dist/scripts/lib/wrangler-table.js.map +1 -0
  63. package/dist/scripts/migrations-lock.d.ts +3 -0
  64. package/dist/scripts/migrations-lock.d.ts.map +1 -0
  65. package/dist/scripts/migrations-lock.js +46 -0
  66. package/dist/scripts/migrations-lock.js.map +1 -0
  67. package/dist/scripts/prepare-wrangler-config.d.ts +3 -0
  68. package/dist/scripts/prepare-wrangler-config.d.ts.map +1 -0
  69. package/dist/scripts/prepare-wrangler-config.js +50 -0
  70. package/dist/scripts/prepare-wrangler-config.js.map +1 -0
  71. package/dist/scripts/spa-shell.d.ts +41 -0
  72. package/dist/scripts/spa-shell.d.ts.map +1 -0
  73. package/dist/scripts/spa-shell.js +155 -0
  74. package/dist/scripts/spa-shell.js.map +1 -0
  75. package/dist/scripts/validate-locales.d.ts +26 -0
  76. package/dist/scripts/validate-locales.d.ts.map +1 -0
  77. package/dist/scripts/validate-locales.js +350 -0
  78. package/dist/scripts/validate-locales.js.map +1 -0
  79. package/dist/scripts/verify-migrations.d.ts +62 -0
  80. package/dist/scripts/verify-migrations.d.ts.map +1 -0
  81. package/dist/scripts/verify-migrations.js +302 -0
  82. package/dist/scripts/verify-migrations.js.map +1 -0
  83. package/dist/scripts/verify-spa-shell.d.ts +3 -0
  84. package/dist/scripts/verify-spa-shell.d.ts.map +1 -0
  85. package/dist/scripts/verify-spa-shell.js +53 -0
  86. package/dist/scripts/verify-spa-shell.js.map +1 -0
  87. package/dist/scripts/wrangler-config/cli.d.ts +22 -0
  88. package/dist/scripts/wrangler-config/cli.d.ts.map +1 -0
  89. package/dist/scripts/wrangler-config/cli.js +51 -0
  90. package/dist/scripts/wrangler-config/cli.js.map +1 -0
  91. package/dist/scripts/wrangler-config/patches.d.ts +51 -0
  92. package/dist/scripts/wrangler-config/patches.d.ts.map +1 -0
  93. package/dist/scripts/wrangler-config/patches.js +140 -0
  94. package/dist/scripts/wrangler-config/patches.js.map +1 -0
  95. package/dist/scripts/wrangler-config/resources.d.ts +70 -0
  96. package/dist/scripts/wrangler-config/resources.d.ts.map +1 -0
  97. package/dist/scripts/wrangler-config/resources.js +290 -0
  98. package/dist/scripts/wrangler-config/resources.js.map +1 -0
  99. package/dist/scripts/wrangler-config/types.d.ts +103 -0
  100. package/dist/scripts/wrangler-config/types.d.ts.map +1 -0
  101. package/dist/scripts/wrangler-config/types.js +49 -0
  102. package/dist/scripts/wrangler-config/types.js.map +1 -0
  103. package/dist/test/integration-migrations.d.ts +167 -0
  104. package/dist/test/integration-migrations.d.ts.map +1 -0
  105. package/dist/test/integration-migrations.js +171 -0
  106. package/dist/test/integration-migrations.js.map +1 -0
  107. package/dist/test/mocks/cloudflare-workers.d.ts +106 -0
  108. package/dist/test/mocks/cloudflare-workers.d.ts.map +1 -0
  109. package/dist/test/mocks/cloudflare-workers.js +90 -0
  110. package/dist/test/mocks/cloudflare-workers.js.map +1 -0
  111. package/dist/vite.d.ts +117 -0
  112. package/dist/vite.d.ts.map +1 -0
  113. package/dist/vite.js +125 -0
  114. package/dist/vite.js.map +1 -0
  115. package/dist/vitest-web.d.ts +73 -0
  116. package/dist/vitest-web.d.ts.map +1 -0
  117. package/dist/vitest-web.js +72 -0
  118. package/dist/vitest-web.js.map +1 -0
  119. package/dist/vitest.d.ts +92 -0
  120. package/dist/vitest.d.ts.map +1 -0
  121. package/dist/vitest.js +128 -0
  122. package/dist/vitest.js.map +1 -0
  123. package/package.json +58 -3
  124. package/src/eslint-boundaries.ts +169 -0
  125. package/src/eslint-rules.ts +276 -0
  126. package/src/eslint.test.ts +175 -0
  127. package/src/eslint.ts +257 -0
  128. package/src/functions/pages-proxy.test.ts +72 -0
  129. package/src/functions/pages-proxy.ts +187 -0
  130. package/src/github/actions/retry-step/action.yml +39 -0
  131. package/src/github/actions/setup-env/action.yml +20 -0
  132. package/src/github/dependabot.yml +30 -0
  133. package/src/github/workflows/backup-main.yml +46 -0
  134. package/src/github/workflows/continuous-deployment.yml +188 -0
  135. package/src/github/workflows/continuous-integration.yml +259 -0
  136. package/src/github/workflows/scheduled-version-update.yml +38 -0
  137. package/src/github/workflows/upstream-sync.yml +56 -0
  138. package/src/index.ts +42 -0
  139. package/src/scripts/backup/backup-rules.test.ts +105 -0
  140. package/src/scripts/backup/d1-target.ts +54 -0
  141. package/src/scripts/backup/destination-config.ts +92 -0
  142. package/src/scripts/backup/encrypt-backup.ts +107 -0
  143. package/src/scripts/backup/naming.ts +62 -0
  144. package/src/scripts/check-god-files.test.ts +131 -0
  145. package/src/scripts/check-god-files.ts +327 -0
  146. package/src/scripts/ensure-spa-shell-stub.ts +34 -0
  147. package/src/scripts/init-secrets.ts +265 -0
  148. package/src/scripts/lib/cli-args.ts +154 -0
  149. package/src/scripts/lib/github-actions.ts +41 -0
  150. package/src/scripts/lib/wrangler-table.ts +105 -0
  151. package/src/scripts/migrations-lock.ts +52 -0
  152. package/src/scripts/prepare-wrangler-config.ts +51 -0
  153. package/src/scripts/spa-shell.test.ts +91 -0
  154. package/src/scripts/spa-shell.ts +179 -0
  155. package/src/scripts/validate-locales.test.ts +89 -0
  156. package/src/scripts/validate-locales.ts +380 -0
  157. package/src/scripts/verify-migrations.test.ts +71 -0
  158. package/src/scripts/verify-migrations.ts +364 -0
  159. package/src/scripts/verify-spa-shell.ts +56 -0
  160. package/src/scripts/wrangler-config/cli.ts +51 -0
  161. package/src/scripts/wrangler-config/patches.ts +157 -0
  162. package/src/scripts/wrangler-config/resources.ts +330 -0
  163. package/src/scripts/wrangler-config/types.ts +113 -0
  164. package/src/test/integration-migrations.test.ts +169 -0
  165. package/src/test/integration-migrations.ts +267 -0
  166. package/src/test/mocks/cloudflare-workers.ts +115 -0
  167. package/src/vite.test.ts +83 -0
  168. package/src/vite.ts +202 -0
  169. package/src/vitest-web.ts +109 -0
  170. 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"}
@@ -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"}