@rubytech/create-maxy-code 0.1.491 → 0.1.492

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 (26) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +46 -0
  3. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  4. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts +2 -0
  5. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts.map +1 -0
  6. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js +135 -0
  7. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js.map +1 -0
  8. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +7 -1
  9. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  10. package/payload/platform/lib/storage-broker/dist/cf-exec.js +26 -14
  11. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  12. package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts +19 -0
  13. package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts.map +1 -0
  14. package/payload/platform/lib/storage-broker/dist/pages-output-dir.js +185 -0
  15. package/payload/platform/lib/storage-broker/dist/pages-output-dir.js.map +1 -0
  16. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +65 -0
  17. package/payload/platform/lib/storage-broker/src/__tests__/pages-output-dir.test.ts +167 -0
  18. package/payload/platform/lib/storage-broker/src/cf-exec.ts +40 -16
  19. package/payload/platform/lib/storage-broker/src/pages-output-dir.ts +194 -0
  20. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +2 -0
  21. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +8 -4
  22. package/payload/platform/plugins/cloudflare/references/hosting-sites.md +1 -1
  23. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +4 -4
  24. package/payload/server/{chunk-KZFE3MOY.js → chunk-RPUTKSAJ.js} +134 -16
  25. package/payload/server/server.js +5 -5
  26. package/payload/server/{src-UYDFMWF4.js → src-JN6PIAJN.js} +1 -1
@@ -0,0 +1,194 @@
1
+ // Where a Pages deploy's assets actually start. Task 1917 — `wrangler pages
2
+ // deploy <dir>` treats the positional argument as the asset root and overrides
3
+ // `pages_build_output_dir`, so handing it the site root publishes a declared
4
+ // output dir one level too deep and the served root 404s. Measured on glsmith
5
+ // 2026-07-22: two trees with identical shape and config, one deployed by house
6
+ // wrangler (document at the root, 200) and one by this broker (document
7
+ // reachable only under /public/, root 404).
8
+ //
9
+ // Only the one key is interpreted, in any of the three config formats. This is
10
+ // deliberately not a wrangler config parser: everything else in the file is
11
+ // wrangler's business and stays wrangler's business.
12
+
13
+ import { lstat, readFile, realpath } from "node:fs/promises";
14
+ import { isAbsolute, join, relative, resolve } from "node:path";
15
+
16
+ // The filenames `wrangler pages deploy` discovers as its own configuration, in
17
+ // wrangler's own precedence order (wrangler 4.112.0, wrangler-dist/cli.js:3189
18
+ // — json, then jsonc, then toml). The order is load-bearing twice: it decides
19
+ // which config declares the output dir here, and Task 1782 drops all three from
20
+ // the upload so a config is never served publicly at /wrangler.toml, where it
21
+ // would disclose the D1 database id and R2 bucket name.
22
+ export const WRANGLER_CONFIG_NAMES = [
23
+ "wrangler.json",
24
+ "wrangler.jsonc",
25
+ "wrangler.toml",
26
+ ] as const;
27
+
28
+ export type UploadDirSource = (typeof WRANGLER_CONFIG_NAMES)[number] | "site-root";
29
+
30
+ export type UploadDirResolution = {
31
+ /** Absolute path to stage and hand wrangler as the asset root. */
32
+ uploadDir: string;
33
+ /** Which input decided it, so the caller logs the reason and not the value alone. */
34
+ source: UploadDirSource;
35
+ /** The verbatim declared value, present only when a config decided it. */
36
+ declared?: string;
37
+ };
38
+
39
+ const KEY = "pages_build_output_dir";
40
+
41
+ /** Strips line comments and block comments that sit outside string literals.
42
+ * JSON has no comments and jsonc does, so a jsonc file is de-commented before
43
+ * JSON.parse. Scanning character-wise rather than by regex is what keeps a
44
+ * `//` inside a value (a path like "a//b") from being eaten as a comment. */
45
+ function stripJsonComments(text: string): string {
46
+ let out = "";
47
+ let inString = false;
48
+ let escaped = false;
49
+ for (let i = 0; i < text.length; i++) {
50
+ const c = text[i];
51
+ if (inString) {
52
+ out += c;
53
+ if (escaped) escaped = false;
54
+ else if (c === "\\") escaped = true;
55
+ else if (c === '"') inString = false;
56
+ continue;
57
+ }
58
+ if (c === '"') {
59
+ inString = true;
60
+ out += c;
61
+ continue;
62
+ }
63
+ if (c === "/" && text[i + 1] === "/") {
64
+ while (i < text.length && text[i] !== "\n") i++;
65
+ out += "\n";
66
+ continue;
67
+ }
68
+ if (c === "/" && text[i + 1] === "*") {
69
+ i += 2;
70
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
71
+ i++;
72
+ continue;
73
+ }
74
+ out += c;
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /** Reads the one key from a TOML file's top level. Top level means before the
80
+ * first `[section]` header: a key under `[env.preview]` is that environment's,
81
+ * not the deploy's, and reading it would publish the wrong root. Nothing else
82
+ * in the file is interpreted. */
83
+ function readTomlKey(text: string): string | undefined {
84
+ for (const raw of text.split("\n")) {
85
+ const line = raw.trim();
86
+ if (line === "" || line.startsWith("#")) continue;
87
+ if (line.startsWith("[")) return undefined;
88
+ const m = /^pages_build_output_dir\s*=\s*(?:"([^"]*)"|'([^']*)')\s*(?:#.*)?$/.exec(line);
89
+ if (m) return m[1] ?? m[2];
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ /** Reads the one key from a JSON or JSONC file's top level. A present key whose
95
+ * value is not a string is an error rather than an ignore: the tree declared an
96
+ * output dir, and publishing its parent instead is the defect this closes. */
97
+ function readJsonKey(text: string, name: string, jsonc: boolean): string | undefined {
98
+ let parsed: unknown;
99
+ try {
100
+ parsed = JSON.parse(jsonc ? stripJsonComments(text) : text);
101
+ } catch (err) {
102
+ throw new Error(`storage-broker: ${name} could not be parsed: ${String(err)}`);
103
+ }
104
+ if (typeof parsed !== "object" || parsed === null) {
105
+ throw new Error(`storage-broker: ${name} is not an object`);
106
+ }
107
+ const value = (parsed as Record<string, unknown>)[KEY];
108
+ if (value === undefined) return undefined;
109
+ if (typeof value !== "string") {
110
+ throw new Error(`storage-broker: ${name} declares a non-string ${KEY}`);
111
+ }
112
+ return value;
113
+ }
114
+
115
+ /** True when `child` is not `root` or a descendant of it. */
116
+ function escapes(root: string, child: string): boolean {
117
+ const rel = relative(root, child);
118
+ return rel.startsWith("..") || isAbsolute(rel);
119
+ }
120
+
121
+ function outsideError(name: string, declared: string, resolved: string, root: string): Error {
122
+ return new Error(
123
+ `storage-broker: ${name} declares ${KEY}="${declared}", which resolves to ${resolved}, outside the site root ${root}`,
124
+ );
125
+ }
126
+
127
+ /** Resolves the directory whose contents become the served root for `dir`.
128
+ *
129
+ * A tree that declares nothing keeps the site root, which is what every deploy
130
+ * did before Task 1917. A tree that declares an output dir gets that dir. A
131
+ * tree that declares one which cannot be resolved is an error naming the path:
132
+ * falling back to the site root here is exactly the defect this closes, because
133
+ * the deploy then succeeds while serving the wrong root. */
134
+ export async function resolveUploadDir(dir: string): Promise<UploadDirResolution> {
135
+ for (const name of WRANGLER_CONFIG_NAMES) {
136
+ let text: string;
137
+ try {
138
+ text = await readFile(join(dir, name), "utf8");
139
+ } catch {
140
+ continue; // absent, or not readable as a file: the next name is wrangler's next choice
141
+ }
142
+ const declared =
143
+ name === "wrangler.toml"
144
+ ? readTomlKey(text)
145
+ : readJsonKey(text, name, name === "wrangler.jsonc");
146
+ // A config that declares nothing leaves the site root as the asset root,
147
+ // which is wrangler's own behaviour for a Pages project with no output dir.
148
+ if (declared === undefined) return { uploadDir: dir, source: "site-root" };
149
+ const uploadDir = resolve(dir, declared);
150
+ // Confinement, lexically first. A declared value is file content, and
151
+ // resolving file content into a publish path through the route that holds
152
+ // the account-wide Cloudflare credential is the moment it has to be
153
+ // bounded. This pass costs no filesystem call and rejects the plain
154
+ // `"../.."` before anything is touched.
155
+ if (escapes(dir, uploadDir)) throw outsideError(name, declared, uploadDir, dir);
156
+ // lstat, not stat: the link itself is what matters here, and stat would
157
+ // report a link to a directory as a directory.
158
+ let info;
159
+ try {
160
+ info = await lstat(uploadDir);
161
+ } catch {
162
+ throw new Error(
163
+ `storage-broker: ${name} declares ${KEY}="${declared}" but ${uploadDir} does not exist`,
164
+ );
165
+ }
166
+ // A link is refused wherever it points. The staging copy takes the upload
167
+ // directory as its source and does not dereference, so a link source fails
168
+ // the copy outright; and a target that can change between this check and
169
+ // that copy is not something to publish from a route holding the
170
+ // account-wide credential.
171
+ if (info.isSymbolicLink()) {
172
+ throw new Error(
173
+ `storage-broker: ${name} declares ${KEY}="${declared}" but ${uploadDir} is a symbolic link; the published root must be a real directory`,
174
+ );
175
+ }
176
+ if (!info.isDirectory()) {
177
+ throw new Error(
178
+ `storage-broker: ${name} declares ${KEY}="${declared}" but ${uploadDir} is not a directory`,
179
+ );
180
+ }
181
+ // Confinement again, on the real paths. The lexical pass above is blind to
182
+ // symlinks in the declared value's own parent segments: a declared "a/b"
183
+ // whose `a` links out of the tree stays inside by string comparison while
184
+ // resolving anywhere on disk, and the deploy would then publish whatever it
185
+ // resolves to. Both sides are resolved because the site root itself commonly
186
+ // arrives through a symlinked prefix (macOS /var, a linked account
187
+ // directory), which would otherwise read as an escape on its own.
188
+ const realDir = await realpath(dir);
189
+ const realUpload = await realpath(uploadDir);
190
+ if (escapes(realDir, realUpload)) throw outsideError(name, declared, realUpload, realDir);
191
+ return { uploadDir, source: name, declared };
192
+ }
193
+ return { uploadDir: dir, source: "site-root" };
194
+ }
@@ -92,6 +92,8 @@ Per-signer stamped copies (§ 6) are written alongside, in the same `<DOC_REF>`
92
92
 
93
93
  The § 1 default — an unguessable hex slug plus `noindex` — keeps a low-sensitivity document private by obscurity. For a **confidential** proposal to a **named** recipient, add a **Cloudflare Access** gate in front of the page so only allowlisted emails can load it at all. Gating is **additive**: the slug and `noindex` stay; the gate sits in front of them. It is never the default and never replaces them.
94
94
 
95
+ > **Client account:** Access gating is the house's to apply. The broker offers no Access tool, and `cf-token.sh access` denies a client account that scope with the same `reason=not-house` it denies `pages`, so mint nothing here and run none of the calls below. Instead **request the gate**, handing the house three things: the **Pages project** carrying the document, the **document path or paths** to gate, and the **signer emails** (plus the business owner's) for the policy `include`. Say in the same request that **`/api/*` on that project is gated alongside those paths**, because gating the document path alone leaves `POST /api/accept` forgeable and `GET /api/status` world-readable, the bypass this section describes below. Never ask the client or the signer for a Cloudflare token (the callout that precedes § 0). Everything that follows (the One-Time PIN pre-flight, the app and policy calls, the `gate-probe`, and the neutral root portal) stays as written and is the house's to run.
96
+
95
97
  **One-Time PIN is the only correct identity provider here — and why.** The signer is an external party who belongs to **no** Cloudflare organization, so SSO, Google, GitHub, SAML and every other org-membership IdP cannot authenticate them. Cloudflare's built-in **One-Time PIN** emails a login code to **any allowlisted address** with no org membership required — exactly the external-signer case. Prescribe `onetimepin`; do not reach for another IdP type for an external signer (a higher-assurance provider is a separate capability, not this skill).
96
98
 
97
99
  **Load-bearing pre-flight — a gate requires an enabled login method.** Creating an Access application and policy does **not** enable a login method. If the org has **zero** identity providers when the gate goes live, the login page offers **nothing** and locks out *everyone, including the operator*. Before activating any Access app, list the org's IdPs and confirm the list is non-empty:
@@ -55,10 +55,14 @@ describe('schema.sql', () => {
55
55
  })
56
56
  })
57
57
 
58
- // The broker runs `wrangler pages deploy <dir>` with <dir> = the site root, so
59
- // the positional directory IS the asset root and pages_build_output_dir is
60
- // overridden. A nested public/ therefore publishes the site one level too deep
61
- // and `/` returns 404 the live failure Task 1774 closes.
58
+ // The template's flat shape and its `pages_build_output_dir = "."` have to agree,
59
+ // because the two together are what puts index.html at the served root. Until
60
+ // Task 1917 the broker handed wrangler the site root as the positional asset
61
+ // directory, which overrode the declared output dir, so a nested public/ served
62
+ // the site one level too deep and `/` returned 404 — the live failure Task 1774
63
+ // closed by flattening. Since 1917 the broker publishes the declared output dir,
64
+ // so a mismatch here no longer 404s; these assertions keep the shipped template
65
+ // on the one shape that is deployed and measured.
62
66
  describe('template layout matches what the deploy publishes', () => {
63
67
  it('has no public/ level', () => {
64
68
  expect(existsSync(join(TEMPLATE, 'public'))).toBe(false)
@@ -33,7 +33,7 @@ If the site captures form data, add the `[[d1_databases]]` binding from `d1-data
33
33
 
34
34
  ## 3. Load the reused deploy token
35
35
 
36
- > **On a multi-tenant install, a client account does not run this step at all.** After the 1631 storage isolation, a client's `cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope (`reason=not-house`) — correctly, since that master is account-wide. A client account publishes with the `storage-pages-deploy` tool, which runs the deploy house-side; the project must already be recorded to the account by the house (`storage-pages-adopt`), because the project-to-folder mapping is not derivable and is never guessed. The rest of this section applies to the **house account and single-tenant installs**, where the master still lives in the account secrets file. If you are a client account and you reached a `not-house` denial here, that is this branch, not a broken token: use the tool.
36
+ > **On a multi-tenant install, a client account does not run this step at all.** After the 1631 storage isolation, a client's `cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope (`reason=not-house`) — correctly, since that master is account-wide. A client account publishes with the `storage-pages-deploy` tool, which runs the deploy house-side; the project must already be recorded to the account by the house (`storage-pages-adopt`), because the project-to-folder mapping is not derivable and is never guessed. The rest of this section applies to the **house account and single-tenant installs**, where the master still lives in the account secrets file. If you are a client account and you reached a `not-house` denial here, that is this branch, not a broken token: use the tool. The tool reads `pages_build_output_dir` from the site root's `wrangler.toml` (or `wrangler.json`/`.jsonc`) and publishes that directory as the served root, which is the same asset root step 4 below passes positionally. A declared output dir that is missing fails the deploy naming the path rather than publishing its parent.
37
37
 
38
38
  A Pages deploy needs **Account · Cloudflare Pages · Edit**. If the site has Pages Functions hitting D1, the token also needs **Account · D1 · Edit** (the single most common breakage — see `d1-data-capture.md`). The `pages` scope resolves the stable `<brand>-pages-d1` token, which carries both. Resolve it with the deterministic helper, which mints it once only if absent and reuses it thereafter. This is the one command; do not source the secrets file and hand the resulting `CLOUDFLARE_API_TOKEN` to `wrangler` — that variable holds the **master**, not the per-scope token, and `wrangler pages` then returns `Authentication error [code: 10000]`:
39
39
 
@@ -98,11 +98,11 @@ Copy `template/` to `<accountDir>/sites/<project>/` and fill `wrangler.toml`'s `
98
98
  placeholder in the assembled tree.**
99
99
 
100
100
  `sites/<project>/` is the only location the broker will publish: `resolveSiteDir` resolves
101
- `<accountDir>/sites/<name>` and refuses anything else with `invalid-site`. The tree is flat because
102
- the deploy publishes the directory it is handed — `index.html`, `portal.css`, `portal.js`,
101
+ `<accountDir>/sites/<name>` and refuses anything else with `invalid-site`. The tree is flat: `index.html`, `portal.css`, `portal.js`,
103
102
  `functions/` and `wrangler.toml` all sit at its root, with no nested asset level, and
104
- `pages_build_output_dir` reads `"."` to match. A nested asset level publishes the site one level too
105
- deep, which serves 404 at the domain root.
103
+ `pages_build_output_dir` reads `"."` to match. The deploy publishes whatever
104
+ directory `pages_build_output_dir` declares, so the two must agree; a nested asset level that the
105
+ config does not declare publishes the site one level too deep, which serves 404 at the domain root.
106
106
 
107
107
  The deploy serves everything in that tree except a fixed ignore list — `functions/`, `_worker.js`,
108
108
  `_redirects`, `_headers`, `_routes.json`, `node_modules`, `.git`, `.DS_Store`, `.wrangler` — and
@@ -92,19 +92,19 @@ function readHouseCredential(platformRoot) {
92
92
  import { execFile as execFile2 } from "child_process";
93
93
  import { cp, mkdtemp, rm } from "fs/promises";
94
94
  import { tmpdir } from "os";
95
- import { join as join2, relative } from "path";
95
+ import { join as join3, relative as relative2 } from "path";
96
96
 
97
97
  // ../lib/storage-broker/src/house-scoped-token.ts
98
98
  import { readFileSync as readFileSync2 } from "fs";
99
99
  import { execFile } from "child_process";
100
- var defaultRun = (cmd, args, env) => new Promise((resolve, reject) => {
100
+ var defaultRun = (cmd, args, env) => new Promise((resolve2, reject) => {
101
101
  execFile(cmd, args, { env }, (err, stdout, stderr) => {
102
102
  if (err) {
103
103
  reject(new Error(`${cmd} ${args.join(" ")} failed: ${err.message}
104
104
  ${stderr}`));
105
105
  return;
106
106
  }
107
- resolve({ stdout: stdout.toString() });
107
+ resolve2({ stdout: stdout.toString() });
108
108
  });
109
109
  });
110
110
  function readKey(path, key) {
@@ -145,8 +145,123 @@ function keyForScope(scope) {
145
145
  }
146
146
  }
147
147
 
148
+ // ../lib/storage-broker/src/pages-output-dir.ts
149
+ import { lstat, readFile, realpath } from "fs/promises";
150
+ import { isAbsolute, join as join2, relative, resolve } from "path";
151
+ var WRANGLER_CONFIG_NAMES = [
152
+ "wrangler.json",
153
+ "wrangler.jsonc",
154
+ "wrangler.toml"
155
+ ];
156
+ var KEY = "pages_build_output_dir";
157
+ function stripJsonComments(text) {
158
+ let out = "";
159
+ let inString = false;
160
+ let escaped = false;
161
+ for (let i = 0; i < text.length; i++) {
162
+ const c = text[i];
163
+ if (inString) {
164
+ out += c;
165
+ if (escaped) escaped = false;
166
+ else if (c === "\\") escaped = true;
167
+ else if (c === '"') inString = false;
168
+ continue;
169
+ }
170
+ if (c === '"') {
171
+ inString = true;
172
+ out += c;
173
+ continue;
174
+ }
175
+ if (c === "/" && text[i + 1] === "/") {
176
+ while (i < text.length && text[i] !== "\n") i++;
177
+ out += "\n";
178
+ continue;
179
+ }
180
+ if (c === "/" && text[i + 1] === "*") {
181
+ i += 2;
182
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
183
+ i++;
184
+ continue;
185
+ }
186
+ out += c;
187
+ }
188
+ return out;
189
+ }
190
+ function readTomlKey(text) {
191
+ for (const raw of text.split("\n")) {
192
+ const line = raw.trim();
193
+ if (line === "" || line.startsWith("#")) continue;
194
+ if (line.startsWith("[")) return void 0;
195
+ const m = /^pages_build_output_dir\s*=\s*(?:"([^"]*)"|'([^']*)')\s*(?:#.*)?$/.exec(line);
196
+ if (m) return m[1] ?? m[2];
197
+ }
198
+ return void 0;
199
+ }
200
+ function readJsonKey(text, name, jsonc) {
201
+ let parsed;
202
+ try {
203
+ parsed = JSON.parse(jsonc ? stripJsonComments(text) : text);
204
+ } catch (err) {
205
+ throw new Error(`storage-broker: ${name} could not be parsed: ${String(err)}`);
206
+ }
207
+ if (typeof parsed !== "object" || parsed === null) {
208
+ throw new Error(`storage-broker: ${name} is not an object`);
209
+ }
210
+ const value = parsed[KEY];
211
+ if (value === void 0) return void 0;
212
+ if (typeof value !== "string") {
213
+ throw new Error(`storage-broker: ${name} declares a non-string ${KEY}`);
214
+ }
215
+ return value;
216
+ }
217
+ function escapes(root, child) {
218
+ const rel = relative(root, child);
219
+ return rel.startsWith("..") || isAbsolute(rel);
220
+ }
221
+ function outsideError(name, declared, resolved, root) {
222
+ return new Error(
223
+ `storage-broker: ${name} declares ${KEY}="${declared}", which resolves to ${resolved}, outside the site root ${root}`
224
+ );
225
+ }
226
+ async function resolveUploadDir(dir) {
227
+ for (const name of WRANGLER_CONFIG_NAMES) {
228
+ let text;
229
+ try {
230
+ text = await readFile(join2(dir, name), "utf8");
231
+ } catch {
232
+ continue;
233
+ }
234
+ const declared = name === "wrangler.toml" ? readTomlKey(text) : readJsonKey(text, name, name === "wrangler.jsonc");
235
+ if (declared === void 0) return { uploadDir: dir, source: "site-root" };
236
+ const uploadDir = resolve(dir, declared);
237
+ if (escapes(dir, uploadDir)) throw outsideError(name, declared, uploadDir, dir);
238
+ let info;
239
+ try {
240
+ info = await lstat(uploadDir);
241
+ } catch {
242
+ throw new Error(
243
+ `storage-broker: ${name} declares ${KEY}="${declared}" but ${uploadDir} does not exist`
244
+ );
245
+ }
246
+ if (info.isSymbolicLink()) {
247
+ throw new Error(
248
+ `storage-broker: ${name} declares ${KEY}="${declared}" but ${uploadDir} is a symbolic link; the published root must be a real directory`
249
+ );
250
+ }
251
+ if (!info.isDirectory()) {
252
+ throw new Error(
253
+ `storage-broker: ${name} declares ${KEY}="${declared}" but ${uploadDir} is not a directory`
254
+ );
255
+ }
256
+ const realDir = await realpath(dir);
257
+ const realUpload = await realpath(uploadDir);
258
+ if (escapes(realDir, realUpload)) throw outsideError(name, declared, realUpload, realDir);
259
+ return { uploadDir, source: name, declared };
260
+ }
261
+ return { uploadDir: dir, source: "site-root" };
262
+ }
263
+
148
264
  // ../lib/storage-broker/src/cf-exec.ts
149
- var WRANGLER_CONFIG_NAMES = ["wrangler.json", "wrangler.jsonc", "wrangler.toml"];
150
265
  function encodeObjectKey(key) {
151
266
  return key.split("/").map(encodeURIComponent).join("/");
152
267
  }
@@ -154,14 +269,14 @@ function objectUrl(accountId, bucket, key) {
154
269
  return `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${encodeURIComponent(bucket)}/objects/${encodeObjectKey(key)}`;
155
270
  }
156
271
  var defaultFetch = (url, init) => fetch(url, init);
157
- var defaultRun2 = (cmd, args, env, opts) => new Promise((resolve, reject) => {
272
+ var defaultRun2 = (cmd, args, env, opts) => new Promise((resolve2, reject) => {
158
273
  execFile2(cmd, args, { env, cwd: opts?.cwd }, (err, stdout, stderr) => {
159
274
  if (err) {
160
275
  reject(new Error(`${cmd} ${args.join(" ")} failed: ${err.message}
161
276
  ${stderr}`));
162
277
  return;
163
278
  }
164
- resolve({ stdout: stdout.toString() });
279
+ resolve2({ stdout: stdout.toString() });
165
280
  });
166
281
  });
167
282
  function parseJson(raw, context) {
@@ -373,15 +488,18 @@ function makeCfExec(cred, run = defaultRun2, fetchFn = defaultFetch) {
373
488
  }
374
489
  },
375
490
  async pagesDeploy(dir, project, branch) {
376
- const staged = await mkdtemp(join2(tmpdir(), "maxy-pages-upload-"));
491
+ const { uploadDir, source: uploadDirSource } = await resolveUploadDir(dir);
492
+ const staged = await mkdtemp(join3(tmpdir(), "maxy-pages-upload-"));
377
493
  const excluded = [];
378
494
  try {
379
- await cp(dir, staged, {
495
+ await cp(uploadDir, staged, {
380
496
  recursive: true,
381
- // Only a config at the root of the tree is wrangler's own. A nested
382
- // file of the same name is site content and stays published.
497
+ // Only a config at the root of what is uploaded is dropped. That root
498
+ // is the served root, which is the surface Task 1782 protects. A file
499
+ // of the same name anywhere below it is site content and stays
500
+ // published.
383
501
  filter: (src) => {
384
- const rel = relative(dir, src);
502
+ const rel = relative2(uploadDir, src);
385
503
  if (!WRANGLER_CONFIG_NAMES.includes(rel)) return true;
386
504
  excluded.push(rel);
387
505
  return false;
@@ -396,7 +514,7 @@ function makeCfExec(cred, run = defaultRun2, fetchFn = defaultFetch) {
396
514
  if (!url) {
397
515
  throw new Error(`storage-broker: pages deploy returned no deployment url: ${stdout}`);
398
516
  }
399
- return { url, excluded: excluded.sort() };
517
+ return { url, excluded: excluded.sort(), uploadDir, uploadDirSource };
400
518
  } finally {
401
519
  await rm(staged, { recursive: true, force: true });
402
520
  }
@@ -477,15 +595,15 @@ ${await res.text()}`
477
595
  }
478
596
  async function makeHouseCfExec(platformRoot, run = defaultRun2, fetchFn = defaultFetch) {
479
597
  const cred = readHouseCredential(platformRoot);
480
- const houseEnvPath = join2(platformRoot, "config", "cloudflare-house.env");
481
- const cfTokenSh = join2(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
598
+ const houseEnvPath = join3(platformRoot, "config", "cloudflare-house.env");
599
+ const cfTokenSh = join3(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
482
600
  const scoped = await resolveHouseScopedToken("storage", houseEnvPath, cfTokenSh, run);
483
601
  return makeCfExec({ apiToken: scoped, accountId: cred.accountId }, run, fetchFn);
484
602
  }
485
603
  async function makeHousePagesExec(platformRoot, run = defaultRun2, fetchFn = defaultFetch) {
486
604
  const cred = readHouseCredential(platformRoot);
487
- const houseEnvPath = join2(platformRoot, "config", "cloudflare-house.env");
488
- const cfTokenSh = join2(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
605
+ const houseEnvPath = join3(platformRoot, "config", "cloudflare-house.env");
606
+ const cfTokenSh = join3(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
489
607
  const scoped = await resolveHouseScopedToken("pages", houseEnvPath, cfTokenSh, run);
490
608
  return makeCfExec({ apiToken: scoped, accountId: cred.accountId }, run, fetchFn);
491
609
  }
@@ -133,7 +133,7 @@ import {
133
133
  registerResource,
134
134
  resolveOwner,
135
135
  tooLargeMessage
136
- } from "./chunk-KZFE3MOY.js";
136
+ } from "./chunk-RPUTKSAJ.js";
137
137
  import {
138
138
  __commonJS,
139
139
  __toESM
@@ -9660,9 +9660,9 @@ app3.post("/pages/deploy", pagesDeployBodyLimit, async (c) => {
9660
9660
  `[pages-broker] op=project-ensure pub=${pub} project=${project} existed=${existed} created=${!existed}`
9661
9661
  );
9662
9662
  const started = Date.now();
9663
- const { url, excluded } = await exec.pagesDeploy(dir, project, branch);
9663
+ const { url, excluded, uploadDir, uploadDirSource } = await exec.pagesDeploy(dir, project, branch);
9664
9664
  console.error(
9665
- `[pages-broker] op=stage pub=${pub} excluded=${excluded.length > 0 ? excluded.join(",") : "none"}`
9665
+ `[pages-broker] op=stage pub=${pub} upload=${uploadDir} uploadFrom=${uploadDirSource} excluded=${excluded.length > 0 ? excluded.join(",") : "none"}`
9666
9666
  );
9667
9667
  console.error(`[pages-broker] op=deploy pub=${pub} ranMs=${Date.now() - started} url=${url}`);
9668
9668
  const httpStatus = await verifyDeployed(url);
@@ -10032,7 +10032,7 @@ app3.post("/r2/object/delete", envelopeBodyLimit("write"), async (c) => {
10032
10032
  });
10033
10033
  var storage_broker_default = app3;
10034
10034
  async function runStorageAudit(root) {
10035
- const { reconcileStorage } = await import("./src-UYDFMWF4.js");
10035
+ const { reconcileStorage } = await import("./src-JN6PIAJN.js");
10036
10036
  const cf = await makeHouseCfExec(root);
10037
10037
  const accountsDir = resolve11(root, "..", "data", "accounts");
10038
10038
  const strays = (await reconcileStorage({
@@ -10072,7 +10072,7 @@ async function runStorageAudit(root) {
10072
10072
  );
10073
10073
  }
10074
10074
  async function runPagesAudit(root) {
10075
- const { reconcilePages } = await import("./src-UYDFMWF4.js");
10075
+ const { reconcilePages } = await import("./src-JN6PIAJN.js");
10076
10076
  const cf = await makeHousePagesExec(root);
10077
10077
  const r = await reconcilePages({
10078
10078
  cfProjects: async () => (await cf.pagesProjectList()).map((p) => p.name),
@@ -23,7 +23,7 @@ import {
23
23
  stripAccountWideTokens,
24
24
  tooLargeMessage,
25
25
  validateMapping
26
- } from "./chunk-KZFE3MOY.js";
26
+ } from "./chunk-RPUTKSAJ.js";
27
27
  import "./chunk-PFF6I7KP.js";
28
28
  export {
29
29
  D1_MAX_SQL_STATEMENT_BYTES,