@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.
- package/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +46 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js +135 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +7 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +26 -14
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts +19 -0
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.js +185 -0
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +65 -0
- package/payload/platform/lib/storage-broker/src/__tests__/pages-output-dir.test.ts +167 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +40 -16
- package/payload/platform/lib/storage-broker/src/pages-output-dir.ts +194 -0
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +2 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +8 -4
- package/payload/platform/plugins/cloudflare/references/hosting-sites.md +1 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +4 -4
- package/payload/server/{chunk-KZFE3MOY.js → chunk-RPUTKSAJ.js} +134 -16
- package/payload/server/server.js +5 -5
- 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
|
|
59
|
-
// the
|
|
60
|
-
//
|
|
61
|
-
//
|
|
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
|
|
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.
|
|
105
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
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(
|
|
495
|
+
await cp(uploadDir, staged, {
|
|
380
496
|
recursive: true,
|
|
381
|
-
// Only a config at the root of
|
|
382
|
-
//
|
|
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 =
|
|
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 =
|
|
481
|
-
const cfTokenSh =
|
|
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 =
|
|
488
|
-
const cfTokenSh =
|
|
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
|
}
|
package/payload/server/server.js
CHANGED
|
@@ -133,7 +133,7 @@ import {
|
|
|
133
133
|
registerResource,
|
|
134
134
|
resolveOwner,
|
|
135
135
|
tooLargeMessage
|
|
136
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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),
|