@rubytech/create-maxy-code 0.1.490 → 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 +30 -8
- 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
|
+
}
|
|
@@ -22,6 +22,10 @@ Auth for every `wrangler`/API call is the **reused per-scope narrow token** of `
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
> **Client accounts on a multi-tenant install — every Cloudflare call in this skill runs through the storage broker.** A client sub-account is issued no Cloudflare credential: after the 1631 storage isolation its `secrets/cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope with `reason=not-house`. That denial is the boundary working, not a broken token, and it is never worked around by minting, by sourcing the house env, or by asking anyone for a token. Read every `cf-token.sh` and `wrangler d1 execute` line below as the **house / single-tenant** path; each one names its client substitute beside it. The map is one-to-one — `storage-d1-create` for a new database, `storage-d1-query` for every SQL statement, `storage-pages-create` to claim a brand-new Pages project, `storage-pages-deploy` to publish it. `storage-d1-query` runs `wrangler d1 execute <name> --remote --json` house-side, so every SQL statement in this skill transfers verbatim and the result is always JSON. The one exception is § 1a Access gating, which needs an Access-scoped token the broker does not offer: a client account that needs a gated document asks the house to apply the gate.
|
|
26
|
+
>
|
|
27
|
+
> **Never ask a client, a signer, or any client contact to create or send a Cloudflare token.** Provisioning a master is an operator action on the house account. A credential request sent to the person you are building the document for is a failure of this skill, not a remediation for one.
|
|
28
|
+
|
|
25
29
|
## 0. Pre-flight — block on each prerequisite
|
|
26
30
|
|
|
27
31
|
Do **not** deploy until all three pass. Each failure has one exact remediation; stop and give it.
|
|
@@ -31,7 +35,13 @@ Do **not** deploy until all three pass. Each failure has one exact remediation;
|
|
|
31
35
|
|
|
32
36
|
Dispatch (§ 6) sends through `email-send`; without a configured inbox the acceptance is captured but never delivered.
|
|
33
37
|
|
|
34
|
-
2. **The Cloudflare master token must exist.**
|
|
38
|
+
2. **The Cloudflare master token must exist — house and single-tenant accounts only.** Which case you are in is a property of the account, so read it rather than assume it:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
jq -r '.role // "unset"' "${ACCOUNT_DIR}/account.json"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`client` means the broker path (the callout above): the master is absent by design, this item does not apply, and you go straight to item 3. Anything else is the house or single-tenant path, which runs the check below. If that command instead errors `Could not open file /account.json`, `ACCOUNT_DIR` is unset — that is item 3's failure surfacing early, so settle item 3 first and come back rather than guessing which path you are on. With `SECRETS_DIR` per `api.md` (`~/<brand>-code/data/accounts/<accountId>/secrets`):
|
|
35
45
|
|
|
36
46
|
```bash
|
|
37
47
|
( test -s "${SECRETS_DIR}/cloudflare.env" && grep -q '^CLOUDFLARE_API_TOKEN=' "${SECRETS_DIR}/cloudflare.env" ) \
|
|
@@ -41,6 +51,8 @@ Do **not** deploy until all three pass. Each failure has one exact remediation;
|
|
|
41
51
|
On `missing`, stop:
|
|
42
52
|
> "There's no Cloudflare master token. Provision it once per `cloudflare/references/api.md` § Provisioning the master token, then ask me again."
|
|
43
53
|
|
|
54
|
+
That provisioning is an operator action on the house account. Never relay it onward, and never ask anyone outside the operator seat to create or send a Cloudflare token — not a client, not a signer, not a client's own hosting contact.
|
|
55
|
+
|
|
44
56
|
3. **The account directory must be set (attachment capability).** `email-send` attaches the signed PDF by absolute path under the account directory, and § 5 persists the base PDF there. If `ACCOUNT_DIR` is unset, the attachment silently degrades to a **text-only** send — neither party receives the PDF — and there is nowhere to persist the base render. Assert it before any deploy or sweep:
|
|
45
57
|
|
|
46
58
|
```bash
|
|
@@ -80,6 +92,8 @@ Per-signer stamped copies (§ 6) are written alongside, in the same `<DOC_REF>`
|
|
|
80
92
|
|
|
81
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.
|
|
82
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
|
+
|
|
83
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).
|
|
84
98
|
|
|
85
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:
|
|
@@ -298,6 +312,8 @@ document.getElementById("esign").addEventListener("submit", async (e) => {
|
|
|
298
312
|
|
|
299
313
|
Follow `d1-data-capture.md` for creating the database, the `wrangler.toml` binding, and the both-Pages-Edit-**and**-D1-Edit token. The schema here is generic — it imposes no business fields; operator-defined fields live in `captured_json`:
|
|
300
314
|
|
|
315
|
+
> **Client account:** create the database with `storage-d1-create` if `storage-d1-list` does not already show it, then run the exact statement below through `storage-d1-query` (`name` is the database, `sql` is the statement verbatim). The schema, the column order, and both uniqueness guards are unchanged — including the "keep `doc_ref` defined before `token`" rule below, which is a property of the SQL, not of how it is executed.
|
|
316
|
+
|
|
301
317
|
```bash
|
|
302
318
|
CLOUDFLARE_API_TOKEN="${MINTED_PAGES_D1}" wrangler d1 execute <db-name> --remote --command \
|
|
303
319
|
"CREATE TABLE IF NOT EXISTS acceptances (
|
|
@@ -426,6 +442,8 @@ export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => {
|
|
|
426
442
|
|
|
427
443
|
Deploy the document + both Functions via `hosting-sites.md`, with the both-Pages-Edit-and-D1-Edit token. The `[[d1_databases]]` binding in `wrangler.toml` is what wires the deployed Functions to the `acceptances` database.
|
|
428
444
|
|
|
445
|
+
> **Client account:** claim a brand-new Pages project with `storage-pages-create`, then publish it with `storage-pages-deploy` from a folder under the account's own `sites/`. Create first — a `storage-pages-deploy` against a project with no owner on record fails at `reason=unregistered`. A project that already exists on Cloudflare and was not created by this account is the house-adopt case (`storage-pages-adopt`, house admin only), so ask the house rather than re-creating it under a second name. The deploy itself runs house-side, so the Pages credential never enters the account. Keep `wrangler.toml` with its `[[d1_databases]]` binding at the root of that site folder exactly as the house does: the broker copies the tree to a staging dir with any root `wrangler.toml` filtered out and points wrangler at the copy, so the binding stays fully in force while the config is never published (a published `wrangler.toml` discloses the D1 database id and the R2 bucket name, both of which embed the account id). The site name is a single path segment; a name containing `/` is refused. Everything after the deploy — the base render, reading every rendered page, the handed-back `https://` URL — is unchanged.
|
|
446
|
+
|
|
429
447
|
**Gated sites also ship the neutral root `index.html` (§ 1a).** When the document is Access-gated, the neutral portal goes in the **same Pages output dir** as the document(s), so the production deploy serves it at `/` in the same shot — no separate deploy. Redeploying *only* the root portal needs a **Pages-Write-only** token: the portal touches no database, so no D1 scope is minted for it.
|
|
430
448
|
|
|
431
449
|
**Render the base PDF once, here, keyed by `DOC_REF`.** Immediately after the deploy, render the live document to PDF a single time and persist it under the account directory (§ 1). This is the *only* place the browser is used — `browser-navigate` to the deployed document URL, then `browser-pdf-save`, the `invoicing.md` step-4 pattern:
|
|
@@ -447,7 +465,9 @@ Before persisting, **read every rendered page** of `base.pdf` — full-bleed pag
|
|
|
447
465
|
|
|
448
466
|
Dispatch runs **only** inside a live operator session on the account's own device — when the operator asks for new acceptances, or when an in-session watcher running inside a still-live session performs the sweep. There is no off-device dispatcher; never register a Claude Code `/schedule` routine or any headless/cloud scheduler to run it (§ 8.2 says why). Mint one **D1-Edit** token here — `${MINTED_D1_EDIT}` — per `api.md` § Minting a narrow token, resolving the permission-group id as `d1-data-capture.md` § 0 does. Reads use the Edit token too: the D1 **query** endpoint rejects a D1-Read token (`d1-data-capture.md`), so a separate read-scoped token is not minted.
|
|
449
467
|
|
|
450
|
-
|
|
468
|
+
> **Client account:** mint nothing. Both statements in this section run through `storage-d1-query` against the same database, executed house-side, and the read returns JSON either way. Everything else about dispatch is identical: it still runs only inside a live operator session, it still stamps the persisted base, and it still flips `swept` one row at a time after both sends verify.
|
|
469
|
+
|
|
470
|
+
1. **Read the unswept rows** with `${MINTED_D1_EDIT}` (client account: the same `SELECT` through `storage-d1-query`):
|
|
451
471
|
|
|
452
472
|
```bash
|
|
453
473
|
CLOUDFLARE_API_TOKEN="${MINTED_D1_EDIT}" wrangler d1 execute <db-name> --remote --json --command \
|
|
@@ -495,7 +515,7 @@ Dispatch runs **only** inside a live operator session on the account's own devic
|
|
|
495
515
|
|
|
496
516
|
**Owner-copy visibility.** The owner copy only *arrives* if the agent's mailbox actually receives mail addressed to the business address. A recipient-filter that drops mail to the business address (the bug fixed in `rubytech-shared-catchall-mailbox`) makes the owner copy vanish while the signer copy arrives — check that filter first if the owner reports a missing confirmation.
|
|
497
517
|
|
|
498
|
-
- **Mark this row — and only this row — swept, once both sends return a verified result**, keyed by its `id
|
|
518
|
+
- **Mark this row — and only this row — swept, once both sends return a verified result**, keyed by its `id` (client account: the same `UPDATE` through `storage-d1-query`):
|
|
499
519
|
|
|
500
520
|
```bash
|
|
501
521
|
CLOUDFLARE_API_TOKEN="${MINTED_D1_EDIT}" wrangler d1 execute <db-name> --remote --command \
|
|
@@ -512,6 +532,8 @@ The acceptance flow is proven end-to-end on the **live** Pages deployment — th
|
|
|
512
532
|
|
|
513
533
|
**When the document is Access-gated (§ 1a), `/api/*` is gated too** — so the `curl` POSTs to `/api/accept` and the `GET /api/status` below are themselves **Access-challenged (302 to login), not direct 200s**. Run this end-to-end verification either **before** the gate is applied, or from a context that can authenticate to Access — the same authenticated-session requirement § 5's base render already carries. The `wrangler d1 execute` rows reach D1 through the Cloudflare API, not the gated host, so they verify unchanged.
|
|
514
534
|
|
|
535
|
+
> **Client account:** the `curl` steps are unchanged — they hit the public Pages host and need no credential. Every `wrangler d1 execute` step below runs through `storage-d1-query` with the same SQL, and the row counts it must return are the same. The contract is the D1 row either way, never the POST status.
|
|
536
|
+
|
|
515
537
|
```bash
|
|
516
538
|
# 1. POST a first acceptance to the live Function.
|
|
517
539
|
curl -sS -X POST -H "Content-Type: application/json" \
|
|
@@ -547,7 +569,7 @@ Surface every step as verb + target ("inserting a test acceptance", "sweeping un
|
|
|
547
569
|
|
|
548
570
|
## 8. Failure modes and the signal for each
|
|
549
571
|
|
|
550
|
-
The flow has no application logging — the signal surface is the **D1 row state** plus the persisted-artifact filesystem, reconciled by standing checks. Every `wrangler d1 execute` below uses the **D1-Edit** token. The failure modes that emit no event are the ones this section exists to cover.
|
|
572
|
+
The flow has no application logging — the signal surface is the **D1 row state** plus the persisted-artifact filesystem, reconciled by standing checks. Every `wrangler d1 execute` below uses the **D1-Edit** token; on a client account every one of them runs through `storage-d1-query` with the same SQL instead, and § 8.9 is the house's to run because Access has no broker tool. The failure modes that emit no event are the ones this section exists to cover.
|
|
551
573
|
|
|
552
574
|
### 8.1 POST silently 500s at the insert (token missing D1 Edit)
|
|
553
575
|
|
|
@@ -559,7 +581,7 @@ The sweep never ran, or `email-send` failed after the row existed — no log, be
|
|
|
559
581
|
|
|
560
582
|
> Dispatch runs **only** inside a live operator session on the account's own device. Never register a Claude Code `/schedule` routine, a `RemoteTrigger`, a `CronCreate` durable job, or any off-device/headless scheduler to run the sweep. `/schedule` spawns a fresh agent on **claude.ai's cloud**, which has none of this account's D1 token, `email-send`, `stamp.mjs`, or `ACCOUNT_DIR` — it physically cannot stamp or send. It is a Claude Code capability, not a Maxy one, and it is not a substitute for the in-session sweep. Monitoring and dispatch happen in-session, while the session is live. That is the only path.
|
|
561
583
|
|
|
562
|
-
The reconciliation query, run inside a live session at open and before close:
|
|
584
|
+
The reconciliation query, run inside a live session at open and before close (client account: through `storage-d1-query`):
|
|
563
585
|
|
|
564
586
|
```bash
|
|
565
587
|
CLOUDFLARE_API_TOKEN="${MINTED_D1_EDIT}" wrangler d1 execute <db-name> --remote --command \
|
|
@@ -585,7 +607,7 @@ d="${ACCOUNT_DIR}/e-sign/<DOC_REF>"
|
|
|
585
607
|
|
|
586
608
|
### 8.4 Duplicate-acceptance row by token (same-signer idempotency regressed)
|
|
587
609
|
|
|
588
|
-
Signal: a `token` appears more than once — the `token`-UNIQUE handling regressed.
|
|
610
|
+
Signal: a `token` appears more than once — the `token`-UNIQUE handling regressed (client account: through `storage-d1-query`).
|
|
589
611
|
|
|
590
612
|
```bash
|
|
591
613
|
CLOUDFLARE_API_TOKEN="${MINTED_D1_EDIT}" wrangler d1 execute <db-name> --remote --command \
|
|
@@ -596,7 +618,7 @@ Healthy signature: re-POSTing a token leaves its count at one.
|
|
|
596
618
|
|
|
597
619
|
### 8.5 More than one acceptance per `doc_ref` (the lock failed — primary standing check)
|
|
598
620
|
|
|
599
|
-
The lock's failure is a **no-event** failure: an extra row appears silently — the `doc_ref` guard was dropped on a redeploy, never applied to a pre-existing table, or a race slipped through. This is the primary lock check; run it on the same cadence as the § 8.2 reconciliation, reusing the same D1-Edit token:
|
|
621
|
+
The lock's failure is a **no-event** failure: an extra row appears silently — the `doc_ref` guard was dropped on a redeploy, never applied to a pre-existing table, or a race slipped through. This is the primary lock check; run it on the same cadence as the § 8.2 reconciliation, reusing the same D1-Edit token (client account: through `storage-d1-query`):
|
|
600
622
|
|
|
601
623
|
```bash
|
|
602
624
|
CLOUDFLARE_API_TOKEN="${MINTED_D1_EDIT}" wrangler d1 execute <db-name> --remote --command \
|
|
@@ -635,7 +657,7 @@ CLOUDFLARE_API_TOKEN="${MINTED_ACCESS}" curl -sS \
|
|
|
635
657
|
|
|
636
658
|
Run on the same cadence as the § 8.2 reconciliation, but with an **Access-scoped** token — not the `${MINTED_D1_EDIT}` the other §8 checks share (this is a `curl` to the Access API, not a `wrangler d1` query). Two adjacent failures are **not** this one and need a different fix: a signer **refused after** the login page offered One-Time PIN is an allowlist miss (their email is absent from the policy `include`, § 1a); a signer who never receives the code despite the method being offered is an email-deliverability problem, not a gate defect. Gating is configuration state, so dispatch is unaffected — the sweep still stamps the persisted base (§ 1a, § 6), never the gated page.
|
|
637
659
|
|
|
638
|
-
**Ground-truth lifeline** for one document's whole acceptance lifecycle:
|
|
660
|
+
**Ground-truth lifeline** for one document's whole acceptance lifecycle (client account: through `storage-d1-query`):
|
|
639
661
|
|
|
640
662
|
```bash
|
|
641
663
|
CLOUDFLARE_API_TOKEN="${MINTED_D1_EDIT}" wrangler d1 execute <db-name> --remote --json --command \
|
|
@@ -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),
|