@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
|
@@ -26,15 +26,54 @@ const throwingRun: RunFn = async () => {
|
|
|
26
26
|
function recordingFetch(response: { ok: boolean; status: number; body: string }) {
|
|
27
27
|
const calls: Array<{
|
|
28
28
|
url: string;
|
|
29
|
-
init: { method: string; headers: Record<string, string>; body?: string };
|
|
29
|
+
init: { method: string; headers: Record<string, string>; body?: string | Uint8Array };
|
|
30
30
|
}> = [];
|
|
31
31
|
const fetchFn: FetchFn = async (url, init) => {
|
|
32
32
|
calls.push({ url, init });
|
|
33
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
ok: response.ok,
|
|
35
|
+
status: response.status,
|
|
36
|
+
text: async () => response.body,
|
|
37
|
+
// Task 1691 — r2ObjectGet reads the raw object body, so the seam carries
|
|
38
|
+
// arrayBuffer alongside text. The fake derives it from the same body.
|
|
39
|
+
arrayBuffer: async () => new TextEncoder().encode(response.body).buffer as ArrayBuffer,
|
|
40
|
+
};
|
|
34
41
|
};
|
|
35
42
|
return { fetchFn, calls };
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
// r2List follows the R2 pagination cursor, so its tests need a fetch that
|
|
46
|
+
// returns a different response per call. recordingFetch returns one fixed
|
|
47
|
+
// response for every call and stays as-is for the single-request paths.
|
|
48
|
+
function sequenceFetch(responses: Array<{ ok: boolean; status: number; body: string }>) {
|
|
49
|
+
const calls: Array<{
|
|
50
|
+
url: string;
|
|
51
|
+
init: { method: string; headers: Record<string, string>; body?: string | Uint8Array };
|
|
52
|
+
}> = [];
|
|
53
|
+
const fetchFn: FetchFn = async (url, init) => {
|
|
54
|
+
calls.push({ url, init });
|
|
55
|
+
const r = responses[calls.length - 1];
|
|
56
|
+
if (!r) throw new Error(`unexpected fetch call #${calls.length} to ${url}`);
|
|
57
|
+
return {
|
|
58
|
+
ok: r.ok,
|
|
59
|
+
status: r.status,
|
|
60
|
+
text: async () => r.body,
|
|
61
|
+
arrayBuffer: async () => new TextEncoder().encode(r.body).buffer as ArrayBuffer,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
return { fetchFn, calls };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function bucketPage(names: string[], cursor?: string) {
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
success: true,
|
|
70
|
+
errors: [],
|
|
71
|
+
messages: [],
|
|
72
|
+
result: { buckets: names.map((name) => ({ name, creation_date: "2026-01-01T00:00:00Z" })) },
|
|
73
|
+
result_info: cursor === undefined ? {} : { cursor, per_page: 20 },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
38
77
|
test("d1List parses --json output and injects the house token", async () => {
|
|
39
78
|
const { run, calls } = recordingRun(JSON.stringify([{ name: "gls-leads", uuid: "u1" }]));
|
|
40
79
|
const cf = makeCfExec(cred, run);
|
|
@@ -72,7 +111,9 @@ test("d1Create creates the database via the D1 API with the house token, never w
|
|
|
72
111
|
assert.equal(calls[0].init.method, "POST");
|
|
73
112
|
assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
|
|
74
113
|
assert.equal(calls[0].init.headers["Content-Type"], "application/json");
|
|
75
|
-
|
|
114
|
+
// init.body widened to string | Uint8Array for the object-put path (Task
|
|
115
|
+
// 1691); d1Create still sends a JSON string.
|
|
116
|
+
assert.deepEqual(JSON.parse(String(calls[0].init.body ?? "")), { name: "gls-new" });
|
|
76
117
|
});
|
|
77
118
|
|
|
78
119
|
test("d1Create throws with the API error body on a non-2xx response", async () => {
|
|
@@ -156,12 +197,138 @@ test("r2List throws on a 200 whose body is not a success envelope, retaining the
|
|
|
156
197
|
await assert.rejects(() => cf.r2List(), /unexpected/);
|
|
157
198
|
});
|
|
158
199
|
|
|
200
|
+
// `null` is valid JSON, so it survives parseJson and reaches the envelope
|
|
201
|
+
// guard as a null parsed value. It must still surface its body like every other
|
|
202
|
+
// malformed 2xx shape, never a bare TypeError with the body dropped.
|
|
203
|
+
test("a 2xx body of JSON null throws with the body, not a TypeError", async () => {
|
|
204
|
+
const { fetchFn } = recordingFetch({ ok: true, status: 200, body: "null" });
|
|
205
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
206
|
+
await assert.rejects(() => cf.r2List(), {
|
|
207
|
+
message: "storage-broker: r2 bucket list API error: null",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
159
211
|
test("non-JSON output throws with the raw body", async () => {
|
|
160
212
|
const { run } = recordingRun("not json");
|
|
161
213
|
const cf = makeCfExec(cred, run);
|
|
162
214
|
await assert.rejects(() => cf.d1List(), /not json/);
|
|
163
215
|
});
|
|
164
216
|
|
|
217
|
+
// Task 1665 — the R2 List-Buckets endpoint paginates by cursor. A single
|
|
218
|
+
// unpaginated request silently truncates the bucket set on a large house
|
|
219
|
+
// account, and the audit reads a truncated list as "those buckets are absent",
|
|
220
|
+
// under-reporting strays. per_page is never sent: its default and cap are
|
|
221
|
+
// unconfirmed, and omitting it makes the loop correct without guessing.
|
|
222
|
+
test("r2List follows the pagination cursor and assembles every page", async () => {
|
|
223
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
224
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets", "gls-docs"], "cur-1") },
|
|
225
|
+
{ ok: true, status: 200, body: bucketPage(["acme-assets"]) },
|
|
226
|
+
]);
|
|
227
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
228
|
+
assert.deepEqual(await cf.r2List(), [
|
|
229
|
+
{ name: "gls-assets" },
|
|
230
|
+
{ name: "gls-docs" },
|
|
231
|
+
{ name: "acme-assets" },
|
|
232
|
+
]);
|
|
233
|
+
assert.equal(calls.length, 2);
|
|
234
|
+
// The first request carries no query string; only follow-ups carry the cursor.
|
|
235
|
+
assert.equal(calls[0].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets");
|
|
236
|
+
assert.equal(
|
|
237
|
+
calls[1].url,
|
|
238
|
+
"https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets?cursor=cur-1",
|
|
239
|
+
);
|
|
240
|
+
assert.equal(calls[1].init.headers.Authorization, "Bearer cfat_x");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("r2List never sends per_page", async () => {
|
|
244
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
245
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-1") },
|
|
246
|
+
{ ok: true, status: 200, body: bucketPage(["gls-docs"]) },
|
|
247
|
+
]);
|
|
248
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
249
|
+
await cf.r2List();
|
|
250
|
+
for (const c of calls) assert.equal(c.url.includes("per_page"), false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("r2List stops on an empty-string cursor", async () => {
|
|
254
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
255
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets"], "") },
|
|
256
|
+
]);
|
|
257
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
258
|
+
assert.deepEqual(await cf.r2List(), [{ name: "gls-assets" }]);
|
|
259
|
+
assert.equal(calls.length, 1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("r2List percent-encodes the cursor", async () => {
|
|
263
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
264
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets"], "a b+c/d=") },
|
|
265
|
+
{ ok: true, status: 200, body: bucketPage([]) },
|
|
266
|
+
]);
|
|
267
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
268
|
+
await cf.r2List();
|
|
269
|
+
assert.equal(
|
|
270
|
+
calls[1].url,
|
|
271
|
+
"https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets?cursor=a%20b%2Bc%2Fd%3D",
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("r2List stops on a null cursor", async () => {
|
|
276
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
277
|
+
{
|
|
278
|
+
ok: true,
|
|
279
|
+
status: 200,
|
|
280
|
+
body: JSON.stringify({
|
|
281
|
+
success: true,
|
|
282
|
+
errors: [],
|
|
283
|
+
messages: [],
|
|
284
|
+
result: { buckets: [{ name: "gls-assets" }] },
|
|
285
|
+
result_info: { cursor: null },
|
|
286
|
+
}),
|
|
287
|
+
},
|
|
288
|
+
]);
|
|
289
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
290
|
+
assert.deepEqual(await cf.r2List(), [{ name: "gls-assets" }]);
|
|
291
|
+
assert.equal(calls.length, 1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("r2List throws on a repeated cursor instead of looping forever", async () => {
|
|
295
|
+
const { fetchFn } = sequenceFetch([
|
|
296
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-1") },
|
|
297
|
+
{ ok: true, status: 200, body: bucketPage(["gls-docs"], "cur-1") },
|
|
298
|
+
]);
|
|
299
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
300
|
+
await assert.rejects(() => cf.r2List(), /repeated pagination cursor: cur-1/);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// A cursor cycle of period >= 2 never repeats the immediately-previous cursor,
|
|
304
|
+
// so it would spin forever — re-appending the same buckets each pass — under a
|
|
305
|
+
// guard that only compares against the last cursor seen.
|
|
306
|
+
test("r2List throws on a cursor cycle that does not repeat consecutively", async () => {
|
|
307
|
+
const { fetchFn } = sequenceFetch([
|
|
308
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-a") },
|
|
309
|
+
{ ok: true, status: 200, body: bucketPage(["gls-docs"], "cur-b") },
|
|
310
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-a") },
|
|
311
|
+
]);
|
|
312
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
313
|
+
await assert.rejects(() => cf.r2List(), /repeated pagination cursor: cur-a/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("r2List surfaces a mid-loop failure with its body, never a partial list", async () => {
|
|
317
|
+
const { fetchFn } = sequenceFetch([
|
|
318
|
+
{ ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-1") },
|
|
319
|
+
{
|
|
320
|
+
ok: false,
|
|
321
|
+
status: 403,
|
|
322
|
+
body: JSON.stringify({
|
|
323
|
+
success: false,
|
|
324
|
+
errors: [{ code: 10000, message: "Authentication error" }],
|
|
325
|
+
}),
|
|
326
|
+
},
|
|
327
|
+
]);
|
|
328
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
329
|
+
await assert.rejects(() => cf.r2List(), /403[\s\S]*Authentication error/);
|
|
330
|
+
});
|
|
331
|
+
|
|
165
332
|
// Task 1670 — makeHouseCfExec resolves a scoped storage token from the house
|
|
166
333
|
// minter via cf-token.sh and uses THAT token (never the minter) for wrangler.
|
|
167
334
|
test("makeHouseCfExec runs wrangler with the scoped token, not the minter", async () => {
|
|
@@ -185,3 +352,195 @@ test("makeHouseCfExec runs wrangler with the scoped token, not the minter", asyn
|
|
|
185
352
|
await cf.d1List();
|
|
186
353
|
assert.equal(sawToken, "scoped_tok", "wrangler saw the scoped token, not the minter");
|
|
187
354
|
});
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Task 1691 — R2 object operations. Same discipline as r2List/d1Create: the
|
|
358
|
+
// Cloudflare API behind an injectable fetch, wrangler never shelled (throwingRun
|
|
359
|
+
// proves it), and every failure keeps its body.
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
function objectPage(keys: string[], opts: { cursor?: string; truncated?: boolean } = {}) {
|
|
363
|
+
return JSON.stringify({
|
|
364
|
+
success: true,
|
|
365
|
+
errors: [],
|
|
366
|
+
messages: [],
|
|
367
|
+
result: keys.map((key) => ({
|
|
368
|
+
key,
|
|
369
|
+
size: 10,
|
|
370
|
+
etag: "e1",
|
|
371
|
+
last_modified: "2026-01-01T00:00:00Z",
|
|
372
|
+
})),
|
|
373
|
+
result_info: { cursor: opts.cursor ?? "", is_truncated: opts.truncated ?? false },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
test("r2ObjectList returns a single page and never shells wrangler", async () => {
|
|
378
|
+
const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage(["a.jpg"]) });
|
|
379
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
380
|
+
assert.deepEqual(await cf.r2ObjectList("acc-a-portal"), [
|
|
381
|
+
{ key: "a.jpg", size: 10, etag: "e1", lastModified: "2026-01-01T00:00:00Z" },
|
|
382
|
+
]);
|
|
383
|
+
assert.equal(calls.length, 1);
|
|
384
|
+
assert.equal(
|
|
385
|
+
calls[0].url,
|
|
386
|
+
"https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets/acc-a-portal/objects",
|
|
387
|
+
);
|
|
388
|
+
assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("r2ObjectList never sends per_page", async () => {
|
|
392
|
+
const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) });
|
|
393
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
394
|
+
await cf.r2ObjectList("b");
|
|
395
|
+
assert.equal(calls[0].url.includes("per_page"), false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("r2ObjectList passes prefix through, encoded", async () => {
|
|
399
|
+
const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) });
|
|
400
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
401
|
+
await cf.r2ObjectList("b", "person a/");
|
|
402
|
+
assert.ok(calls[0].url.endsWith("/objects?prefix=person+a%2F"), calls[0].url);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("r2ObjectList follows the cursor to exhaustion", async () => {
|
|
406
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
407
|
+
{ ok: true, status: 200, body: objectPage(["a.jpg"], { cursor: "c1", truncated: true }) },
|
|
408
|
+
{ ok: true, status: 200, body: objectPage(["b.jpg"]) },
|
|
409
|
+
]);
|
|
410
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
411
|
+
assert.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]);
|
|
412
|
+
assert.equal(calls.length, 2);
|
|
413
|
+
assert.ok(calls[1].url.includes("cursor=c1"));
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// is_truncated is the authoritative continuation signal, not the presence of a
|
|
417
|
+
// cursor: a last page may still carry one.
|
|
418
|
+
test("r2ObjectList stops when is_truncated is false even if a cursor is present", async () => {
|
|
419
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
420
|
+
{ ok: true, status: 200, body: objectPage(["a.jpg"], { cursor: "c1", truncated: false }) },
|
|
421
|
+
]);
|
|
422
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
423
|
+
await cf.r2ObjectList("b");
|
|
424
|
+
assert.equal(calls.length, 1);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// The response shape is doc-confirmed but never measured against a live account.
|
|
428
|
+
// If it is not what we understand, that must be loud: a partial list is exactly
|
|
429
|
+
// the bug this loop exists to prevent.
|
|
430
|
+
test("r2ObjectList throws when truncated but no cursor is given, never a partial list", async () => {
|
|
431
|
+
const { fetchFn } = recordingFetch({
|
|
432
|
+
ok: true,
|
|
433
|
+
status: 200,
|
|
434
|
+
body: objectPage(["a.jpg"], { truncated: true }),
|
|
435
|
+
});
|
|
436
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
437
|
+
await assert.rejects(() => cf.r2ObjectList("b"), /truncated but gave no pagination cursor/);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("r2ObjectList throws on a repeated cursor", async () => {
|
|
441
|
+
const { fetchFn } = sequenceFetch([
|
|
442
|
+
{ ok: true, status: 200, body: objectPage(["a"], { cursor: "c1", truncated: true }) },
|
|
443
|
+
{ ok: true, status: 200, body: objectPage(["b"], { cursor: "c1", truncated: true }) },
|
|
444
|
+
]);
|
|
445
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
446
|
+
await assert.rejects(() => cf.r2ObjectList("b"), /repeated pagination cursor/);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("r2ObjectList retains the body on a non-2xx", async () => {
|
|
450
|
+
const { fetchFn } = recordingFetch({ ok: false, status: 403, body: '{"errors":[{"code":10000}]}' });
|
|
451
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
452
|
+
await assert.rejects(() => cf.r2ObjectList("b"), /10000/);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("r2ObjectGet returns exact bytes and encodes the key segment-wise", async () => {
|
|
456
|
+
// Binary that is not valid UTF-8: proves the body never round-trips through
|
|
457
|
+
// text(), which would corrupt it.
|
|
458
|
+
const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff]);
|
|
459
|
+
const calls: string[] = [];
|
|
460
|
+
const fetchFn: FetchFn = async (url) => {
|
|
461
|
+
calls.push(url);
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
status: 200,
|
|
465
|
+
text: async () => "unused",
|
|
466
|
+
arrayBuffer: async () => bytes.buffer.slice(0) as ArrayBuffer,
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
470
|
+
const got = await cf.r2ObjectGet("b", "person a/photo 1.jpg");
|
|
471
|
+
assert.deepEqual(Array.from(got), Array.from(bytes));
|
|
472
|
+
// Separators stay separators; spaces are escaped.
|
|
473
|
+
assert.ok(calls[0].endsWith("/objects/person%20a/photo%201.jpg"), calls[0]);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("r2ObjectGet retains the body on a non-2xx", async () => {
|
|
477
|
+
const { fetchFn } = recordingFetch({ ok: false, status: 404, body: "no such key" });
|
|
478
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
479
|
+
await assert.rejects(() => cf.r2ObjectGet("b", "k"), /no such key/);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("r2ObjectPut sends the bytes as the request body", async () => {
|
|
483
|
+
const { fetchFn, calls } = recordingFetch({
|
|
484
|
+
ok: true,
|
|
485
|
+
status: 200,
|
|
486
|
+
body: JSON.stringify({ success: true, errors: [], messages: [], result: {} }),
|
|
487
|
+
});
|
|
488
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
489
|
+
const bytes = new Uint8Array([1, 2, 3]);
|
|
490
|
+
await cf.r2ObjectPut("b", "k.bin", bytes);
|
|
491
|
+
assert.equal(calls[0].init.method, "PUT");
|
|
492
|
+
assert.deepEqual(calls[0].init.body, bytes);
|
|
493
|
+
assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("r2ObjectPut retains the body on a non-success envelope", async () => {
|
|
497
|
+
const { fetchFn } = recordingFetch({
|
|
498
|
+
ok: true,
|
|
499
|
+
status: 200,
|
|
500
|
+
body: '{"success":false,"errors":[{"code":10001}]}',
|
|
501
|
+
});
|
|
502
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
503
|
+
await assert.rejects(() => cf.r2ObjectPut("b", "k", new Uint8Array([1])), /10001/);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("r2ObjectDelete issues a DELETE", async () => {
|
|
507
|
+
const { fetchFn, calls } = recordingFetch({
|
|
508
|
+
ok: true,
|
|
509
|
+
status: 200,
|
|
510
|
+
body: JSON.stringify({ success: true, errors: [], messages: [], result: {} }),
|
|
511
|
+
});
|
|
512
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
513
|
+
await cf.r2ObjectDelete("b", "k");
|
|
514
|
+
assert.equal(calls[0].init.method, "DELETE");
|
|
515
|
+
assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("r2ObjectDelete retains the body on a non-2xx", async () => {
|
|
519
|
+
const { fetchFn } = recordingFetch({ ok: false, status: 500, body: "boom" });
|
|
520
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
521
|
+
await assert.rejects(() => cf.r2ObjectDelete("b", "k"), /boom/);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Code review — is_truncated is itself an unmeasured fact (no live credential
|
|
525
|
+
// exists to confirm the envelope). Driving the loop solely on it means a
|
|
526
|
+
// response that omits it while still paginating truncates SILENTLY, which is
|
|
527
|
+
// the exact failure this loop exists to prevent, and it would also make
|
|
528
|
+
// r2ObjectList disagree with r2List directly above it, which drives the same
|
|
529
|
+
// envelope on cursor presence alone.
|
|
530
|
+
test("r2ObjectList follows a cursor when is_truncated is absent, rather than truncating silently", async () => {
|
|
531
|
+
const pageWithoutTruncatedFlag = (keys: string[], cursor: string | null) =>
|
|
532
|
+
JSON.stringify({
|
|
533
|
+
success: true,
|
|
534
|
+
errors: [],
|
|
535
|
+
messages: [],
|
|
536
|
+
result: keys.map((key) => ({ key, size: 1, etag: "e", last_modified: "t" })),
|
|
537
|
+
result_info: cursor === null ? {} : { cursor },
|
|
538
|
+
});
|
|
539
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
540
|
+
{ ok: true, status: 200, body: pageWithoutTruncatedFlag(["a.jpg"], "c1") },
|
|
541
|
+
{ ok: true, status: 200, body: pageWithoutTruncatedFlag(["b.jpg"], null) },
|
|
542
|
+
]);
|
|
543
|
+
const cf = makeCfExec(cred, throwingRun, fetchFn);
|
|
544
|
+
assert.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]);
|
|
545
|
+
assert.equal(calls.length, 2);
|
|
546
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
R2_OBJECT_MAX_BYTES,
|
|
5
|
+
R2_OBJECT_PUT_MAX_BODY_BYTES,
|
|
6
|
+
base64Ceiling,
|
|
7
|
+
tooLargeMessage,
|
|
8
|
+
} from "../object-limits.js";
|
|
9
|
+
|
|
10
|
+
test("the cap is 100 MiB", () => {
|
|
11
|
+
assert.equal(R2_OBJECT_MAX_BYTES, 100 * 1024 * 1024);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// The cap is set independently of email's 25 MiB. This pins that they are not
|
|
15
|
+
// the same number, so a future edit that "unifies" them fails here and has to
|
|
16
|
+
// read the design note rather than silently coupling two unrelated limits.
|
|
17
|
+
test("the cap is not email's attachment cap", () => {
|
|
18
|
+
assert.notEqual(R2_OBJECT_MAX_BYTES, 25 * 1024 * 1024);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("base64Ceiling matches base64's 4-out-per-3-in expansion, rounded up", () => {
|
|
22
|
+
assert.equal(base64Ceiling(0), 0);
|
|
23
|
+
assert.equal(base64Ceiling(1), 4);
|
|
24
|
+
assert.equal(base64Ceiling(3), 4);
|
|
25
|
+
assert.equal(base64Ceiling(4), 8);
|
|
26
|
+
assert.equal(base64Ceiling(6), 8);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// The ceiling must never sit below what a legal at-cap object actually encodes
|
|
30
|
+
// to, or a legitimate put would be rejected by its own encoding.
|
|
31
|
+
test("base64Ceiling is never below Node's own base64 length", () => {
|
|
32
|
+
for (const n of [0, 1, 2, 3, 4, 5, 100, 1023, 4096]) {
|
|
33
|
+
const encoded = Buffer.alloc(n).toString("base64").length;
|
|
34
|
+
assert.ok(
|
|
35
|
+
base64Ceiling(n) >= encoded,
|
|
36
|
+
`base64Ceiling(${n}) = ${base64Ceiling(n)} < ${encoded}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the put body ceiling admits a cap-sized object plus an envelope allowance", () => {
|
|
42
|
+
assert.equal(
|
|
43
|
+
R2_OBJECT_PUT_MAX_BODY_BYTES,
|
|
44
|
+
base64Ceiling(R2_OBJECT_MAX_BYTES) + 64 * 1024,
|
|
45
|
+
);
|
|
46
|
+
assert.ok(R2_OBJECT_PUT_MAX_BODY_BYTES > base64Ceiling(R2_OBJECT_MAX_BYTES));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// The allowance is for a key and JSON punctuation, not for smuggling a bigger
|
|
50
|
+
// object: it must stay far below the cap it guards.
|
|
51
|
+
test("the envelope allowance is negligible against the cap", () => {
|
|
52
|
+
const allowance = R2_OBJECT_PUT_MAX_BODY_BYTES - base64Ceiling(R2_OBJECT_MAX_BYTES);
|
|
53
|
+
assert.ok(allowance < R2_OBJECT_MAX_BYTES / 1000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("the over-cap message names the object and the limit", () => {
|
|
57
|
+
const msg = tooLargeMessage("photo.jpg", 150 * 1024 * 1024);
|
|
58
|
+
assert.match(msg, /photo\.jpg/);
|
|
59
|
+
assert.match(msg, /150\.0 MiB/);
|
|
60
|
+
assert.match(msg, /100\.0 MiB/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Code review — MiB alone rounds, so an object one byte over the cap rendered as
|
|
64
|
+
// "100.0 MiB, which exceeds the 100.0 MiB limit": a message that contradicts
|
|
65
|
+
// itself and reads as a bug rather than a rejection. The exact bytes are what
|
|
66
|
+
// disambiguate it, so they are pinned rather than left to formatting taste.
|
|
67
|
+
test("the message stays legible for an object one byte over the cap", () => {
|
|
68
|
+
const msg = tooLargeMessage("photo.jpg", R2_OBJECT_MAX_BYTES + 1);
|
|
69
|
+
assert.match(msg, /104857601 bytes/);
|
|
70
|
+
assert.match(msg, /104857600 bytes/);
|
|
71
|
+
assert.notEqual(
|
|
72
|
+
msg.match(/100\.0 MiB \(104857601 bytes\)/),
|
|
73
|
+
null,
|
|
74
|
+
"the actual size must carry its exact byte count",
|
|
75
|
+
);
|
|
76
|
+
});
|