@rubytech/create-maxy-code 0.1.457 → 0.1.459
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/mcp-lifeline/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-lifeline/dist/index.js +22 -1
- package/payload/platform/lib/mcp-lifeline/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-lifeline/src/__tests__/lifeline.test.ts +26 -0
- package/payload/platform/lib/mcp-lifeline/src/index.ts +24 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +322 -2
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +59 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +12 -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 +156 -26
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.js +8 -2
- package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +1 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +53 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js +72 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +362 -3
- package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +76 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +230 -44
- package/payload/platform/lib/storage-broker/src/house-credential.ts +8 -2
- package/payload/platform/lib/storage-broker/src/index.ts +1 -0
- package/payload/platform/lib/storage-broker/src/object-limits.ts +75 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -5
- package/payload/platform/plugins/cloudflare/bin/__tests__/cf-token.test.sh +145 -4
- package/payload/platform/plugins/cloudflare/bin/cf-token.sh +61 -3
- package/payload/platform/plugins/cloudflare/references/api.md +1 -1
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/outlook-guide.md +4 -4
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +16 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +108 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +10 -7
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +42 -15
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +32 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +31 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.js +3 -5
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +36 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +56 -9
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +13 -2
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +18 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +3 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js +2 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +41 -6
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
- package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
- package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
- package/payload/platform/scripts/logs-read.sh +63 -9
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
- package/payload/server/{chunk-SDYRKIYY.js → chunk-Q6W4U6HL.js} +153 -27
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +206 -6
- package/payload/server/{src-554BYJMN.js → src-3I2RYZFB.js} +9 -1
|
@@ -14,14 +14,32 @@ export type RunFn = (
|
|
|
14
14
|
env: Record<string, string>,
|
|
15
15
|
) => Promise<{ stdout: string }>;
|
|
16
16
|
|
|
17
|
-
// The HTTP seam for the Cloudflare API. Injectable so
|
|
17
|
+
// The HTTP seam for the Cloudflare API. Injectable so the API-backed methods are
|
|
18
18
|
// unit-testable without a network. Minimal subset of the global fetch: callers
|
|
19
19
|
// read the body once via text() and branch on ok/status. `body` is set for the
|
|
20
|
-
// POST paths (d1Create); GET paths
|
|
20
|
+
// POST/PUT paths (d1Create, r2ObjectPut); GET/DELETE paths omit it.
|
|
21
|
+
//
|
|
22
|
+
// Task 1691 widened both halves: `body` carries Uint8Array for the object-put
|
|
23
|
+
// path, and `arrayBuffer` reads the raw object body for r2ObjectGet, which is
|
|
24
|
+
// the one method whose response is not a JSON envelope. arrayBuffer is required
|
|
25
|
+
// rather than optional: an optional member would force a fake-only fallback
|
|
26
|
+
// branch in r2ObjectGet that never runs against a real fetch.
|
|
21
27
|
export type FetchFn = (
|
|
22
28
|
url: string,
|
|
23
|
-
init: { method: string; headers: Record<string, string>; body?: string },
|
|
24
|
-
) => Promise<{
|
|
29
|
+
init: { method: string; headers: Record<string, string>; body?: string | Uint8Array },
|
|
30
|
+
) => Promise<{
|
|
31
|
+
ok: boolean;
|
|
32
|
+
status: number;
|
|
33
|
+
text(): Promise<string>;
|
|
34
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
export interface R2Object {
|
|
38
|
+
key: string;
|
|
39
|
+
size: number;
|
|
40
|
+
etag: string;
|
|
41
|
+
lastModified: string;
|
|
42
|
+
}
|
|
25
43
|
|
|
26
44
|
export interface CfExec {
|
|
27
45
|
d1List(): Promise<{ name: string; uuid: string }[]>;
|
|
@@ -29,9 +47,33 @@ export interface CfExec {
|
|
|
29
47
|
d1Query(name: string, sql: string): Promise<unknown>;
|
|
30
48
|
r2List(): Promise<{ name: string }[]>;
|
|
31
49
|
r2Create(name: string): Promise<void>;
|
|
50
|
+
r2ObjectList(bucket: string, prefix?: string): Promise<R2Object[]>;
|
|
51
|
+
r2ObjectGet(bucket: string, key: string): Promise<Uint8Array>;
|
|
52
|
+
r2ObjectPut(bucket: string, key: string, body: Uint8Array): Promise<void>;
|
|
53
|
+
r2ObjectDelete(bucket: string, key: string): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// An object key carries `/` as a real path separator (the data portal's
|
|
57
|
+
// per-person prefixes), so it is encoded segment-wise rather than whole:
|
|
58
|
+
// separators stay separators, and spaces and specials are escaped. This
|
|
59
|
+
// encoding is not doc-confirmed; it is chosen so that a wrong guess surfaces as
|
|
60
|
+
// a 404 carrying its body, never as a silently misfiled object.
|
|
61
|
+
function encodeObjectKey(key: string): string {
|
|
62
|
+
return key.split("/").map(encodeURIComponent).join("/");
|
|
32
63
|
}
|
|
33
64
|
|
|
34
|
-
|
|
65
|
+
// The single-object endpoint, shared by get/put/delete so the three cannot drift
|
|
66
|
+
// apart on encoding.
|
|
67
|
+
function objectUrl(accountId: string, bucket: string, key: string): string {
|
|
68
|
+
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${encodeURIComponent(bucket)}/objects/${encodeObjectKey(key)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The cast is confined to this adapter. A Uint8Array is a valid fetch body at
|
|
72
|
+
// runtime, but the DOM lib's BodyInit does not admit TypeScript's generic
|
|
73
|
+
// Uint8Array<ArrayBufferLike>, so consumers compiled against DOM typings (the
|
|
74
|
+
// ui server) reject the call the Node typings accept. Casting here keeps the
|
|
75
|
+
// seam's own type honest for every caller and test.
|
|
76
|
+
const defaultFetch: FetchFn = (url, init) => fetch(url, init as RequestInit);
|
|
35
77
|
|
|
36
78
|
const defaultRun: RunFn = (cmd, args, env) =>
|
|
37
79
|
new Promise((resolve, reject) => {
|
|
@@ -52,6 +94,34 @@ function parseJson(raw: string, context: string): unknown {
|
|
|
52
94
|
}
|
|
53
95
|
}
|
|
54
96
|
|
|
97
|
+
// The Cloudflare-API envelope, in one place. Both API-backed methods (r2List,
|
|
98
|
+
// d1Create) share it: read the body exactly once, surface a non-2xx with its
|
|
99
|
+
// body, parse, then require an explicit `success:true`. A 2xx whose body is not
|
|
100
|
+
// a well-formed success envelope is an anomaly to surface with its body, never
|
|
101
|
+
// a silently empty/uuid-less result. The raw `body` is returned alongside the
|
|
102
|
+
// parsed value so callers can raise their own shaping errors on the original
|
|
103
|
+
// bytes rather than a re-serialised copy.
|
|
104
|
+
async function cfApiJson(
|
|
105
|
+
fetchFn: FetchFn,
|
|
106
|
+
url: string,
|
|
107
|
+
init: { method: string; headers: Record<string, string>; body?: string | Uint8Array },
|
|
108
|
+
context: string,
|
|
109
|
+
): Promise<{ parsed: unknown; body: string }> {
|
|
110
|
+
const res = await fetchFn(url, init);
|
|
111
|
+
const body = await res.text();
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
throw new Error(`storage-broker: ${context} failed: ${res.status}\n${body}`);
|
|
114
|
+
}
|
|
115
|
+
// `null` is valid JSON and survives parseJson, so the guard is optional-
|
|
116
|
+
// chained: a null parsed value must surface its body like any other malformed
|
|
117
|
+
// 2xx shape, never a bare TypeError with the body dropped.
|
|
118
|
+
const parsed = parseJson(body, context) as { success?: boolean } | null;
|
|
119
|
+
if (parsed?.success !== true) {
|
|
120
|
+
throw new Error(`storage-broker: ${context} API error: ${body}`);
|
|
121
|
+
}
|
|
122
|
+
return { parsed, body };
|
|
123
|
+
}
|
|
124
|
+
|
|
55
125
|
export function makeCfExec(
|
|
56
126
|
cred: HouseCredential,
|
|
57
127
|
run: RunFn = defaultRun,
|
|
@@ -76,30 +146,26 @@ export function makeCfExec(
|
|
|
76
146
|
// so shelling it with --json is always rejected. The D1 API creates the
|
|
77
147
|
// database and returns its record — incl. the uuid — as JSON directly.
|
|
78
148
|
const url = `https://api.cloudflare.com/client/v4/accounts/${cred.accountId}/d1/database`;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
149
|
+
const { parsed, body } = await cfApiJson(
|
|
150
|
+
fetchFn,
|
|
151
|
+
url,
|
|
152
|
+
{
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
Authorization: `Bearer ${cred.apiToken}`,
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({ name }),
|
|
84
159
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const parsed = parseJson(body, "d1 create") as {
|
|
92
|
-
success?: boolean;
|
|
93
|
-
errors?: unknown;
|
|
94
|
-
result?: { uuid?: string };
|
|
95
|
-
};
|
|
96
|
-
// Require an explicit success envelope with a uuid. A 2xx whose body is
|
|
97
|
-
// not a well-formed `success:true` response is an anomaly to surface with
|
|
98
|
-
// its body, never a silently uuid-less create.
|
|
99
|
-
if (parsed.success !== true || typeof parsed.result?.uuid !== "string") {
|
|
160
|
+
"d1 create",
|
|
161
|
+
);
|
|
162
|
+
// The envelope guard lives in cfApiJson; a uuid-less 2xx success envelope
|
|
163
|
+
// is still an anomaly to surface with its body, never a silent create.
|
|
164
|
+
const uuid = (parsed as { result?: { uuid?: string } }).result?.uuid;
|
|
165
|
+
if (typeof uuid !== "string") {
|
|
100
166
|
throw new Error(`storage-broker: d1 create API error: ${body}`);
|
|
101
167
|
}
|
|
102
|
-
return { uuid
|
|
168
|
+
return { uuid };
|
|
103
169
|
},
|
|
104
170
|
async d1Query(name, sql) {
|
|
105
171
|
const { stdout } = await run(
|
|
@@ -113,30 +179,150 @@ export function makeCfExec(
|
|
|
113
179
|
// `wrangler r2 bucket list` has no --json flag and emits human prose, so
|
|
114
180
|
// it can neither be parsed nor honour the "never grep a CLI's prose"
|
|
115
181
|
// doctrine. The R2 API returns the bucket set as JSON directly.
|
|
116
|
-
|
|
117
|
-
|
|
182
|
+
//
|
|
183
|
+
// The endpoint paginates: a single request returns only the first page,
|
|
184
|
+
// and the audit reads the missing remainder as "absent", under-reporting
|
|
185
|
+
// strays. Follow result_info.cursor to exhaustion instead. per_page is
|
|
186
|
+
// deliberately never sent — its default and maximum are unconfirmed, and
|
|
187
|
+
// omitting it takes whatever page size Cloudflare serves, so the loop is
|
|
188
|
+
// complete without guessing a value that could 400 the request.
|
|
189
|
+
const base = `https://api.cloudflare.com/client/v4/accounts/${cred.accountId}/r2/buckets`;
|
|
190
|
+
const buckets: { name: string }[] = [];
|
|
191
|
+
const seen = new Set<string>();
|
|
192
|
+
let cursor: string | undefined;
|
|
193
|
+
for (;;) {
|
|
194
|
+
const url = cursor === undefined ? base : `${base}?cursor=${encodeURIComponent(cursor)}`;
|
|
195
|
+
const { parsed } = await cfApiJson(
|
|
196
|
+
fetchFn,
|
|
197
|
+
url,
|
|
198
|
+
{ method: "GET", headers: { Authorization: `Bearer ${cred.apiToken}` } },
|
|
199
|
+
"r2 bucket list",
|
|
200
|
+
);
|
|
201
|
+
const page = parsed as {
|
|
202
|
+
result?: { buckets?: { name: string }[] };
|
|
203
|
+
result_info?: { cursor?: string | null };
|
|
204
|
+
};
|
|
205
|
+
for (const b of page.result?.buckets ?? []) buckets.push({ name: b.name });
|
|
206
|
+
// A missing, null, or empty cursor all mean "last page" — accept every
|
|
207
|
+
// documented last-page signal rather than depend on which one is sent.
|
|
208
|
+
const next = page.result_info?.cursor;
|
|
209
|
+
if (!next) return buckets;
|
|
210
|
+
// Any cursor seen before means the API is cycling rather than advancing,
|
|
211
|
+
// which would loop forever and re-append the same buckets each pass.
|
|
212
|
+
// Tracking every cursor (not just the last) also catches a cycle of
|
|
213
|
+
// period >= 2, which never repeats consecutively. Surface it instead of
|
|
214
|
+
// hanging the broker on an unbounded wait.
|
|
215
|
+
if (seen.has(next)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`storage-broker: r2 bucket list returned a repeated pagination cursor: ${next}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
seen.add(next);
|
|
221
|
+
cursor = next;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
async r2Create(name) {
|
|
225
|
+
await run("npx", ["wrangler", "r2", "bucket", "create", name], env);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// --- R2 objects (Task 1691) -------------------------------------------
|
|
229
|
+
//
|
|
230
|
+
// The v4 REST API exposes the object surface under
|
|
231
|
+
// /accounts/{id}/r2/buckets/{bucket}/objects and accepts the same Bearer
|
|
232
|
+
// token as the bucket endpoints (account-level Workers R2 Storage Write).
|
|
233
|
+
// The r2/api/tokens/ note that "Object Read & Write" is S3-only constrains
|
|
234
|
+
// the bucket-item-scoped *permission types*, not these endpoints.
|
|
235
|
+
//
|
|
236
|
+
// Doc-confirmed, never measured: no house Cloudflare credential exists on
|
|
237
|
+
// the dev machine. The design keeps the unmeasured facts unable to affect
|
|
238
|
+
// correctness — per_page is never sent, and nothing fails silently.
|
|
239
|
+
async r2ObjectList(bucket, prefix) {
|
|
240
|
+
// Mirrors r2List: follow the cursor to exhaustion, never send per_page.
|
|
241
|
+
// A truncated object listing reads as "those objects are absent" to the
|
|
242
|
+
// data portal's standing audit, producing false phantom rows and hiding
|
|
243
|
+
// real orphans — the same bug r2List had, one endpoint over.
|
|
244
|
+
const base = `https://api.cloudflare.com/client/v4/accounts/${cred.accountId}/r2/buckets/${encodeURIComponent(bucket)}/objects`;
|
|
245
|
+
const objects: R2Object[] = [];
|
|
246
|
+
const seen = new Set<string>();
|
|
247
|
+
let cursor: string | undefined;
|
|
248
|
+
for (;;) {
|
|
249
|
+
const params = new URLSearchParams();
|
|
250
|
+
if (prefix !== undefined) params.set("prefix", prefix);
|
|
251
|
+
if (cursor !== undefined) params.set("cursor", cursor);
|
|
252
|
+
const qs = params.toString();
|
|
253
|
+
const { parsed, body } = await cfApiJson(
|
|
254
|
+
fetchFn,
|
|
255
|
+
qs ? `${base}?${qs}` : base,
|
|
256
|
+
{ method: "GET", headers: { Authorization: `Bearer ${cred.apiToken}` } },
|
|
257
|
+
"r2 object list",
|
|
258
|
+
);
|
|
259
|
+
const page = parsed as {
|
|
260
|
+
result?: Array<{ key: string; size: number; etag: string; last_modified: string }>;
|
|
261
|
+
result_info?: { cursor?: string | null; is_truncated?: boolean };
|
|
262
|
+
};
|
|
263
|
+
for (const o of page.result ?? []) {
|
|
264
|
+
objects.push({ key: o.key, size: o.size, etag: o.etag, lastModified: o.last_modified });
|
|
265
|
+
}
|
|
266
|
+
const truncated = page.result_info?.is_truncated;
|
|
267
|
+
const next = page.result_info?.cursor;
|
|
268
|
+
// is_truncated is authoritative *when present*: a last page may still
|
|
269
|
+
// carry a cursor, and following it would issue a pointless request or
|
|
270
|
+
// trip the cycle guard below on a healthy response.
|
|
271
|
+
if (truncated === false) return objects;
|
|
272
|
+
// The API says there is more but names no cursor: the response is not
|
|
273
|
+
// the shape we understand. Surface it rather than return a partial
|
|
274
|
+
// list, which is the failure this loop exists to prevent.
|
|
275
|
+
if (truncated === true && !next) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`storage-broker: r2 object list reported truncated but gave no pagination cursor: ${body}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
// is_truncated absent: it is itself an unmeasured fact, so it must not
|
|
281
|
+
// be what decides correctness. Fall back to cursor presence — the rule
|
|
282
|
+
// r2List uses on the same envelope — so an envelope without the flag
|
|
283
|
+
// paginates rather than truncating silently.
|
|
284
|
+
if (!next) return objects;
|
|
285
|
+
// A cursor seen before means the API is cycling rather than advancing:
|
|
286
|
+
// it would loop forever, re-appending the same objects each pass.
|
|
287
|
+
if (seen.has(next)) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`storage-broker: r2 object list returned a repeated pagination cursor: ${next}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
seen.add(next);
|
|
293
|
+
cursor = next;
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
async r2ObjectGet(bucket, key) {
|
|
297
|
+
// The one method cfApiJson cannot wrap: get returns the raw object body,
|
|
298
|
+
// not a success envelope, so there is nothing to parse or assert
|
|
299
|
+
// success:true on. The error body is still retained on a non-2xx.
|
|
300
|
+
const res = await fetchFn(objectUrl(cred.accountId, bucket, key), {
|
|
118
301
|
method: "GET",
|
|
119
302
|
headers: { Authorization: `Bearer ${cred.apiToken}` },
|
|
120
303
|
});
|
|
121
|
-
const body = await res.text();
|
|
122
304
|
if (!res.ok) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
success?: boolean;
|
|
127
|
-
errors?: unknown;
|
|
128
|
-
result?: { buckets?: { name: string }[] };
|
|
129
|
-
};
|
|
130
|
-
// Require an explicit success envelope. A 2xx whose body is not a
|
|
131
|
-
// well-formed `success:true` response is an anomaly to surface with its
|
|
132
|
-
// body, never a silent empty bucket set.
|
|
133
|
-
if (parsed.success !== true) {
|
|
134
|
-
throw new Error(`storage-broker: r2 bucket list API error: ${body}`);
|
|
305
|
+
throw new Error(
|
|
306
|
+
`storage-broker: r2 object get failed: ${res.status}\n${await res.text()}`,
|
|
307
|
+
);
|
|
135
308
|
}
|
|
136
|
-
return (
|
|
309
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
137
310
|
},
|
|
138
|
-
async
|
|
139
|
-
await
|
|
311
|
+
async r2ObjectPut(bucket, key, body) {
|
|
312
|
+
await cfApiJson(
|
|
313
|
+
fetchFn,
|
|
314
|
+
objectUrl(cred.accountId, bucket, key),
|
|
315
|
+
{ method: "PUT", headers: { Authorization: `Bearer ${cred.apiToken}` }, body },
|
|
316
|
+
"r2 object put",
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
async r2ObjectDelete(bucket, key) {
|
|
320
|
+
await cfApiJson(
|
|
321
|
+
fetchFn,
|
|
322
|
+
objectUrl(cred.accountId, bucket, key),
|
|
323
|
+
{ method: "DELETE", headers: { Authorization: `Bearer ${cred.apiToken}` } },
|
|
324
|
+
"r2 object delete",
|
|
325
|
+
);
|
|
140
326
|
},
|
|
141
327
|
};
|
|
142
328
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// Reads the single account-wide Cloudflare credential from the house-only
|
|
2
2
|
// config directory. This file is the one place the account-wide capability
|
|
3
|
-
// lives
|
|
4
|
-
//
|
|
3
|
+
// lives. House-only is a location convention plus the cf-token.sh caller gate,
|
|
4
|
+
// NOT an OS permission: every account's session runs as the same unix user, so
|
|
5
|
+
// a sub-account agent holding Bash can read this path directly. That is a
|
|
6
|
+
// permanent property, not a pending gap: OS user separation was costed and
|
|
7
|
+
// declined as disproportionate (Task 1690, archived undone), so nothing tracks
|
|
8
|
+
// it. Do not cite this file's location or mode as a boundary. Parses
|
|
9
|
+
// KEY=value lines without polluting process.env (same discipline as
|
|
10
|
+
// reconcile-bookings' readEnvFile).
|
|
5
11
|
|
|
6
12
|
import { readFileSync } from "node:fs";
|
|
7
13
|
import { join } from "node:path";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byte cap on R2 object transfer, and the arithmetic the routes need to
|
|
3
|
+
* enforce it before they buffer anything.
|
|
4
|
+
*
|
|
5
|
+
* One number, one home (Task 1695). The storage-broker MCP tools and the house
|
|
6
|
+
* ui routes both read the cap from here, so the process that rejects a too-large
|
|
7
|
+
* file and the process that would otherwise buffer it cannot drift apart.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately dependency-free: the MCP imports this module directly, without
|
|
10
|
+
* the rest of the lib (cf-exec, registry, audit, remediate), which it has no use
|
|
11
|
+
* for. The ui route reads it through the barrel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The maximum size, in bytes, of an object moved through storage-r2-object-get
|
|
16
|
+
* or storage-r2-object-put.
|
|
17
|
+
*
|
|
18
|
+
* Set independently of the email plugin's OUTBOUND_ATTACHMENT_MAX_BYTES (25 MiB)
|
|
19
|
+
* even though the payload class is the same. The two exist for different reasons
|
|
20
|
+
* and must be free to move for different reasons: email's tracks an SMTP-side
|
|
21
|
+
* rejection threshold, this one tracks house-process memory. Do not couple them.
|
|
22
|
+
*
|
|
23
|
+
* Not bounded by R2. A single-part upload may be up to 5 GiB (doc-confirmed:
|
|
24
|
+
* developers.cloudflare.com/r2/platform/limits/), roughly fifty times this, and
|
|
25
|
+
* the v4 REST API states no separate object-size limit. This cap is the only
|
|
26
|
+
* bound on either transfer.
|
|
27
|
+
*
|
|
28
|
+
* The bound is not small: at the cap a get still leaves the house process
|
|
29
|
+
* holding the object and its base64 form, roughly 2.4x the object. That cost was
|
|
30
|
+
* weighed and accepted when the number was chosen.
|
|
31
|
+
*/
|
|
32
|
+
export const R2_OBJECT_MAX_BYTES = 100 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
/** base64 emits 4 characters per 3 input bytes, padded up to the next quantum. */
|
|
35
|
+
export function base64Ceiling(bytes: number): number {
|
|
36
|
+
return 4 * Math.ceil(bytes / 3);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Room for the put envelope's bucket, key and JSON punctuation, so a legal
|
|
41
|
+
* at-cap put is not rejected by its own key. Far above any realistic bucket+key
|
|
42
|
+
* length and far below the cap, so it cannot smuggle a meaningfully larger
|
|
43
|
+
* object.
|
|
44
|
+
*/
|
|
45
|
+
const PUT_ENVELOPE_ALLOWANCE_BYTES = 64 * 1024;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The largest JSON body /r2/object/put can legitimately carry.
|
|
49
|
+
*
|
|
50
|
+
* This bounds house memory; it does not adjudicate the last kilobyte. The exact
|
|
51
|
+
* decoded-byte rule lives in the MCP, which knows the source's size exactly.
|
|
52
|
+
*/
|
|
53
|
+
export const R2_OBJECT_PUT_MAX_BODY_BYTES =
|
|
54
|
+
base64Ceiling(R2_OBJECT_MAX_BYTES) + PUT_ENVELOPE_ALLOWANCE_BYTES;
|
|
55
|
+
|
|
56
|
+
function formatMiB(bytes: number): string {
|
|
57
|
+
return (bytes / 1024 / 1024).toFixed(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The over-cap message, worded once so the MCP and the routes agree. Names the
|
|
62
|
+
* offending object and the limit, which is what makes the failure actionable
|
|
63
|
+
* rather than an opaque out-of-memory kill.
|
|
64
|
+
*
|
|
65
|
+
* Exact bytes accompany the MiB figures because MiB alone rounds: an object one
|
|
66
|
+
* byte over the cap reads "100.0 MiB, which exceeds the 100.0 MiB limit", which
|
|
67
|
+
* is a message that appears to contradict itself and invites the reader to treat
|
|
68
|
+
* the rejection as a bug. The bytes are what make it legible; the MiB is what
|
|
69
|
+
* makes it quick.
|
|
70
|
+
*/
|
|
71
|
+
export function tooLargeMessage(what: string, actualBytes: number): string {
|
|
72
|
+
return `${what} is ${formatMiB(actualBytes)} MiB (${actualBytes} bytes), which exceeds the ${formatMiB(
|
|
73
|
+
R2_OBJECT_MAX_BYTES,
|
|
74
|
+
)} MiB (${R2_OBJECT_MAX_BYTES} bytes) R2 object limit.`;
|
|
75
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:15c9a9d54bd8898a18c541ce95b9f5dc869646faad096eefc60741bb405a062d
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -2071,9 +2071,9 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
2071
2071
|
| `outlook-contacts-list` | Top contacts. Default top=50. |
|
|
2072
2072
|
| `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
|
|
2073
2073
|
| `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
|
|
2074
|
-
| `outlook-mail-send` | Send a message
|
|
2075
|
-
| `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model. Returns the draft id. Needs `Mail.ReadWrite`. |
|
|
2076
|
-
| `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`). Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml). Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
|
|
2074
|
+
| `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
|
|
2075
|
+
| `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model, including attachments — files are attached before the id is returned, so the draft is ready to send. Returns the draft id. Needs `Mail.ReadWrite`. |
|
|
2076
|
+
| `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments). Attachments are additive; removal is not supported. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
|
|
2077
2077
|
| `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
|
|
2078
2078
|
|
|
2079
2079
|
## Observability
|
|
@@ -2148,7 +2148,7 @@ Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions iss
|
|
|
2148
2148
|
|
|
2149
2149
|
## Out of scope
|
|
2150
2150
|
|
|
2151
|
-
Mail move to arbitrary folders / flag, hard (permanent) delete, ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
|
|
2151
|
+
Mail move to arbitrary folders / flag, hard (permanent) delete, removing or replacing an attachment already on a draft, ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose — all four outbound paths carrying attachments — are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
|
|
2152
2152
|
|
|
2153
2153
|
---
|
|
2154
2154
|
# QuickBooks Online
|
|
@@ -298,8 +298,12 @@ setup "cfat_master_abc"
|
|
|
298
298
|
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
299
299
|
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
|
|
300
300
|
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
|
|
301
|
+
# The caller gate (cases 18-22) admits only the house account, so this case
|
|
302
|
+
# spawns as the house account.
|
|
303
|
+
mkdir -p "$SANDBOX/acme-code/data/accounts/acc123"
|
|
304
|
+
printf '{"role":"house"}\n' > "$SANDBOX/acme-code/data/accounts/acc123/account.json"
|
|
301
305
|
of=$(mktemp); ef=$(mktemp)
|
|
302
|
-
PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
306
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
303
307
|
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
304
308
|
[ "$RC" -eq 0 ] && ok "house fallback resolves (exit=0)" || { bad "house fallback (exit=$RC)"; echo " stderr: $ERR" >&2; }
|
|
305
309
|
[ "$OUT" = "CF_PAGES_D1_TOKEN" ] && ok "house fallback stdout is canonical key" || bad "house out='$OUT'"
|
|
@@ -327,8 +331,9 @@ teardown
|
|
|
327
331
|
|
|
328
332
|
# 15. fail-closed: stripped per-account file and NO house env reachable (no
|
|
329
333
|
# PLATFORM_ROOT, no CF_HOUSE_ENV) -> non-zero, no mint, and a message that
|
|
330
|
-
# names the missing credential source.
|
|
331
|
-
#
|
|
334
|
+
# names the missing credential source. Reaching this branch means no house
|
|
335
|
+
# credential was found at all; the caller gate (cases 18-22) is what decides
|
|
336
|
+
# whether a reachable house credential may actually be used.
|
|
332
337
|
setup "cfat_master_abc"
|
|
333
338
|
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
334
339
|
of=$(mktemp); ef=$(mktemp)
|
|
@@ -345,8 +350,13 @@ setup "cfat_master_abc"
|
|
|
345
350
|
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
346
351
|
OVERRIDE="$SANDBOX/acme-code/override-house.env"
|
|
347
352
|
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$OVERRIDE" )
|
|
353
|
+
# The caller gate admits only the house account, and locates the accounts dir
|
|
354
|
+
# from PLATFORM_ROOT. The PLATFORM_ROOT-derived house path does not exist here,
|
|
355
|
+
# so a pass still proves CF_HOUSE_ENV won.
|
|
356
|
+
mkdir -p "$SANDBOX/acme-code/data/accounts/acc123"
|
|
357
|
+
printf '{"role":"house"}\n' > "$SANDBOX/acme-code/data/accounts/acc123/account.json"
|
|
348
358
|
of=$(mktemp); ef=$(mktemp)
|
|
349
|
-
CF_HOUSE_ENV="$OVERRIDE" CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
359
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_HOUSE_ENV="$OVERRIDE" CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
350
360
|
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
351
361
|
[ "$RC" -eq 0 ] && ok "CF_HOUSE_ENV override resolves (exit=0)" || bad "override (exit=$RC): $ERR"
|
|
352
362
|
grep -q "^CF_PAGES_D1_TOKEN=" "$OVERRIDE" && ok "override env received the minted token" || bad "override not persisted"
|
|
@@ -377,5 +387,136 @@ grep -q "^CF_CALENDAR_D1_TOKEN=" "$SECRETS" && ok "calendar-d1 token persisted"
|
|
|
377
387
|
no_leak "calendar-d1 mint"
|
|
378
388
|
teardown
|
|
379
389
|
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Caller gate (Task 1646). The house env carries an account-wide master, so the
|
|
392
|
+
# fallback must fire only for the house account. "House" mirrors
|
|
393
|
+
# resolveHouseOrSoleAccountId: account.json role:"house", or the sole account.
|
|
394
|
+
# The gate reads the caller's own account.json under
|
|
395
|
+
# $(dirname $PLATFORM_ROOT)/data/accounts/$ACCOUNT_ID/, which the sandbox
|
|
396
|
+
# layout already matches ($SANDBOX/acme-code/data/accounts/acc123).
|
|
397
|
+
|
|
398
|
+
# 18. a sub-account caller must NOT resolve the house master: deny, non-zero,
|
|
399
|
+
# no mint, and nothing persisted to either file.
|
|
400
|
+
setup "cfat_master_abc"
|
|
401
|
+
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
402
|
+
ADIR="$SANDBOX/acme-code/data/accounts"
|
|
403
|
+
mkdir -p "$ADIR/acc123" "$ADIR/house1"
|
|
404
|
+
printf '{"role":"client"}\n' > "$ADIR/acc123/account.json"
|
|
405
|
+
printf '{"role":"house"}\n' > "$ADIR/house1/account.json"
|
|
406
|
+
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
407
|
+
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
|
|
408
|
+
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
|
|
409
|
+
of=$(mktemp); ef=$(mktemp)
|
|
410
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
|
|
411
|
+
bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
412
|
+
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
413
|
+
[ "$RC" -ne 0 ] && ok "sub-account denied the house master (exit=$RC)" || bad "sub-account NOT denied (exit=$RC)"
|
|
414
|
+
grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "minted for a sub-account" || ok "no mint for a sub-account"
|
|
415
|
+
grep -q "^CF_PAGES_D1_TOKEN=" "$HOUSE_ENV" && bad "sub-account wrote a token to house env" || ok "house env untouched on deny"
|
|
416
|
+
grep -q "^CF_PAGES_D1_TOKEN=" "$SECRETS" && bad "sub-account wrote a token to its own file" || ok "per-account file untouched on deny"
|
|
417
|
+
echo "$ERR" | grep -q "action=deny" && ok "deny lifeline emitted" || bad "no deny lifeline: $ERR"
|
|
418
|
+
echo "$ERR" | grep -q "reason=not-house" && ok "deny lifeline names the reason" || bad "no reason: $ERR"
|
|
419
|
+
no_leak "sub-account deny"
|
|
420
|
+
teardown
|
|
421
|
+
|
|
422
|
+
# 19. the house account (role:"house") is allowed, preserving house-run hosting.
|
|
423
|
+
setup "cfat_master_abc"
|
|
424
|
+
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
425
|
+
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/other1"
|
|
426
|
+
printf '{"role":"house"}\n' > "$ADIR/acc123/account.json"
|
|
427
|
+
printf '{"role":"client"}\n' > "$ADIR/other1/account.json"
|
|
428
|
+
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
429
|
+
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
|
|
430
|
+
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
|
|
431
|
+
of=$(mktemp); ef=$(mktemp)
|
|
432
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
|
|
433
|
+
bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
434
|
+
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
435
|
+
[ "$RC" -eq 0 ] && ok "house caller allowed (exit=0)" || { bad "house caller denied (exit=$RC)"; echo " stderr: $ERR" >&2; }
|
|
436
|
+
[ "$OUT" = "CF_PAGES_D1_TOKEN" ] && ok "house caller stdout is canonical key" || bad "house caller out='$OUT'"
|
|
437
|
+
grep -q "^CF_PAGES_D1_TOKEN=" "$HOUSE_ENV" && ok "house caller persisted to house env" || bad "house caller not persisted"
|
|
438
|
+
teardown
|
|
439
|
+
|
|
440
|
+
# 20. the sole unlabelled account IS the house account (resolveHouseOrSoleAccountId's
|
|
441
|
+
# second arm) -> allowed.
|
|
442
|
+
setup "cfat_master_abc"
|
|
443
|
+
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
444
|
+
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123"
|
|
445
|
+
printf '{"name":"only"}\n' > "$ADIR/acc123/account.json"
|
|
446
|
+
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
447
|
+
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
|
|
448
|
+
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
|
|
449
|
+
of=$(mktemp); ef=$(mktemp)
|
|
450
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
|
|
451
|
+
bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
452
|
+
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
453
|
+
[ "$RC" -eq 0 ] && ok "sole unlabelled account allowed (exit=0)" || bad "sole account denied (exit=$RC): $ERR"
|
|
454
|
+
teardown
|
|
455
|
+
|
|
456
|
+
# 21. unset ACCOUNT_ID fails closed.
|
|
457
|
+
setup "cfat_master_abc"
|
|
458
|
+
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
459
|
+
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house1"
|
|
460
|
+
printf '{"role":"client"}\n' > "$ADIR/acc123/account.json"
|
|
461
|
+
printf '{"role":"house"}\n' > "$ADIR/house1/account.json"
|
|
462
|
+
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
463
|
+
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
|
|
464
|
+
of=$(mktemp); ef=$(mktemp)
|
|
465
|
+
env -u ACCOUNT_ID PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
|
|
466
|
+
bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
467
|
+
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
468
|
+
[ "$RC" -ne 0 ] && ok "unset ACCOUNT_ID fails closed (exit=$RC)" || bad "unset ACCOUNT_ID allowed (exit=$RC)"
|
|
469
|
+
echo "$ERR" | grep -q "reason=no-account-id" && ok "no-account-id reason surfaced" || bad "reason: $ERR"
|
|
470
|
+
teardown
|
|
471
|
+
|
|
472
|
+
# 22. a caller whose account.json is absent fails closed.
|
|
473
|
+
setup "cfat_master_abc"
|
|
474
|
+
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
475
|
+
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house1"
|
|
476
|
+
printf '{"role":"house"}\n' > "$ADIR/house1/account.json"
|
|
477
|
+
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
478
|
+
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
|
|
479
|
+
of=$(mktemp); ef=$(mktemp)
|
|
480
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
|
|
481
|
+
bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
482
|
+
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
483
|
+
[ "$RC" -ne 0 ] && ok "missing account.json fails closed (exit=$RC)" || bad "missing account.json allowed"
|
|
484
|
+
echo "$ERR" | grep -q "reason=no-account-json" && ok "no-account-json reason surfaced" || bad "reason: $ERR"
|
|
485
|
+
teardown
|
|
486
|
+
|
|
487
|
+
# 23. two role:"house" accounts is corruption: resolveAccountDir returns null
|
|
488
|
+
# rather than picking one, so the gate denies rather than trusting a
|
|
489
|
+
# self-declared house.
|
|
490
|
+
setup "cfat_master_abc"
|
|
491
|
+
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
492
|
+
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house2"
|
|
493
|
+
printf '{"role":"house"}\n' > "$ADIR/acc123/account.json"
|
|
494
|
+
printf '{"role":"house"}\n' > "$ADIR/house2/account.json"
|
|
495
|
+
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
496
|
+
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
|
|
497
|
+
of=$(mktemp); ef=$(mktemp)
|
|
498
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
|
|
499
|
+
bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
|
|
500
|
+
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
501
|
+
[ "$RC" -ne 0 ] && ok "two houses fails closed (exit=$RC)" || bad "two houses allowed (exit=$RC)"
|
|
502
|
+
echo "$ERR" | grep -q "reason=multiple-house-accounts" && ok "multiple-house reason surfaced" || bad "reason: $ERR"
|
|
503
|
+
teardown
|
|
504
|
+
|
|
505
|
+
# 24. the deny message names the scope actually requested, not a fixed list.
|
|
506
|
+
setup "cfat_master_abc"
|
|
507
|
+
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
|
|
508
|
+
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house1"
|
|
509
|
+
printf '{"role":"client"}\n' > "$ADIR/acc123/account.json"
|
|
510
|
+
printf '{"role":"house"}\n' > "$ADIR/house1/account.json"
|
|
511
|
+
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
|
|
512
|
+
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
|
|
513
|
+
of=$(mktemp); ef=$(mktemp)
|
|
514
|
+
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
|
|
515
|
+
bash "$HELPER" dns "$SECRETS" >"$of" 2>"$ef"
|
|
516
|
+
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
517
|
+
echo "$ERR" | grep -q "scope=dns" && ok "deny lifeline names scope=dns" || bad "lifeline scope: $ERR"
|
|
518
|
+
echo "$ERR" | grep -qF "'dns' scope" && ok "deny message names the dns scope" || bad "deny message scope: $ERR"
|
|
519
|
+
teardown
|
|
520
|
+
|
|
380
521
|
echo "----"; echo "PASS=$PASS FAIL=$FAIL"
|
|
381
522
|
[ "$FAIL" -eq 0 ]
|