@rubytech/create-maxy-code 0.1.458 → 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.
Files changed (116) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +188 -3
  3. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  4. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
  5. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
  6. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +59 -0
  7. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
  8. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +12 -1
  9. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  10. package/payload/platform/lib/storage-broker/dist/cf-exec.js +97 -0
  11. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  12. package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
  13. package/payload/platform/lib/storage-broker/dist/house-credential.js +4 -2
  14. package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
  15. package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
  16. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
  17. package/payload/platform/lib/storage-broker/dist/index.js +1 -0
  18. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
  19. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +53 -0
  20. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
  21. package/payload/platform/lib/storage-broker/dist/object-limits.js +72 -0
  22. package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
  23. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +211 -5
  24. package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +76 -0
  25. package/payload/platform/lib/storage-broker/src/cf-exec.ts +148 -6
  26. package/payload/platform/lib/storage-broker/src/house-credential.ts +4 -2
  27. package/payload/platform/lib/storage-broker/src/index.ts +1 -0
  28. package/payload/platform/lib/storage-broker/src/object-limits.ts +75 -0
  29. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -5
  30. package/payload/platform/plugins/cloudflare/bin/cf-token.sh +4 -3
  31. package/payload/platform/plugins/cloudflare/references/api.md +1 -1
  32. package/payload/platform/plugins/docs/references/outlook-guide.md +4 -4
  33. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  34. package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
  35. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
  36. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
  37. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
  38. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
  39. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
  40. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
  41. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
  42. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
  43. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
  44. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
  46. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
  47. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  48. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  49. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
  50. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
  51. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
  52. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
  53. package/payload/platform/plugins/outlook/mcp/dist/index.js +10 -7
  54. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  55. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
  56. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
  57. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
  58. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
  59. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
  60. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
  61. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
  62. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
  63. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +31 -8
  64. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
  65. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
  66. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
  67. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
  68. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
  69. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
  70. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
  71. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +29 -4
  72. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  73. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +46 -13
  74. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  75. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +8 -2
  76. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  77. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +13 -4
  78. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  79. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  80. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
  81. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  82. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
  83. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
  84. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
  85. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
  86. package/payload/platform/plugins/outlook/references/graph-surfaces.md +29 -6
  87. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
  88. package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
  89. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
  90. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
  91. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
  92. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
  93. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
  94. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
  95. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
  96. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
  97. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
  98. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
  99. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
  100. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
  101. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
  102. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
  103. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
  104. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
  105. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
  106. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
  107. package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
  108. package/payload/platform/scripts/logs-read.sh +63 -9
  109. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  110. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
  111. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  112. package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
  113. package/payload/server/{chunk-64FGYKNZ.js → chunk-Q6W4U6HL.js} +104 -0
  114. package/payload/server/maxy-edge.js +1 -1
  115. package/payload/server/server.js +206 -6
  116. package/payload/server/{src-S7C4P6TT.js → src-3I2RYZFB.js} +9 -1
@@ -22,6 +22,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  __exportStar(require("./authorize.js"), exports);
25
+ __exportStar(require("./object-limits.js"), exports);
25
26
  __exportStar(require("./registry.js"), exports);
26
27
  __exportStar(require("./house-credential.js"), exports);
27
28
  __exportStar(require("./cf-exec.js"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,mEAAmE;AACnE,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,2EAA2E;AAC3E,uEAAuE;AACvE,4DAA4D;;;;;;;;;;;;;;;;AAE5D,iDAA+B;AAC/B,gDAA8B;AAC9B,wDAAsC;AACtC,+CAA6B;AAC7B,0DAAwC;AACxC,6CAA2B;AAC3B,iDAA+B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,mEAAmE;AACnE,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,2EAA2E;AAC3E,uEAAuE;AACvE,4DAA4D;;;;;;;;;;;;;;;;AAE5D,iDAA+B;AAC/B,qDAAmC;AACnC,gDAA8B;AAC9B,wDAAsC;AACtC,+CAA6B;AAC7B,0DAAwC;AACxC,6CAA2B;AAC3B,iDAA+B"}
@@ -0,0 +1,53 @@
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
+ * The maximum size, in bytes, of an object moved through storage-r2-object-get
15
+ * or storage-r2-object-put.
16
+ *
17
+ * Set independently of the email plugin's OUTBOUND_ATTACHMENT_MAX_BYTES (25 MiB)
18
+ * even though the payload class is the same. The two exist for different reasons
19
+ * and must be free to move for different reasons: email's tracks an SMTP-side
20
+ * rejection threshold, this one tracks house-process memory. Do not couple them.
21
+ *
22
+ * Not bounded by R2. A single-part upload may be up to 5 GiB (doc-confirmed:
23
+ * developers.cloudflare.com/r2/platform/limits/), roughly fifty times this, and
24
+ * the v4 REST API states no separate object-size limit. This cap is the only
25
+ * bound on either transfer.
26
+ *
27
+ * The bound is not small: at the cap a get still leaves the house process
28
+ * holding the object and its base64 form, roughly 2.4x the object. That cost was
29
+ * weighed and accepted when the number was chosen.
30
+ */
31
+ export declare const R2_OBJECT_MAX_BYTES: number;
32
+ /** base64 emits 4 characters per 3 input bytes, padded up to the next quantum. */
33
+ export declare function base64Ceiling(bytes: number): number;
34
+ /**
35
+ * The largest JSON body /r2/object/put can legitimately carry.
36
+ *
37
+ * This bounds house memory; it does not adjudicate the last kilobyte. The exact
38
+ * decoded-byte rule lives in the MCP, which knows the source's size exactly.
39
+ */
40
+ export declare const R2_OBJECT_PUT_MAX_BODY_BYTES: number;
41
+ /**
42
+ * The over-cap message, worded once so the MCP and the routes agree. Names the
43
+ * offending object and the limit, which is what makes the failure actionable
44
+ * rather than an opaque out-of-memory kill.
45
+ *
46
+ * Exact bytes accompany the MiB figures because MiB alone rounds: an object one
47
+ * byte over the cap reads "100.0 MiB, which exceeds the 100.0 MiB limit", which
48
+ * is a message that appears to contradict itself and invites the reader to treat
49
+ * the rejection as a bug. The bytes are what make it legible; the MiB is what
50
+ * makes it quick.
51
+ */
52
+ export declare function tooLargeMessage(what: string, actualBytes: number): string;
53
+ //# sourceMappingURL=object-limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-limits.d.ts","sourceRoot":"","sources":["../src/object-limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,QAAoB,CAAC;AAErD,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAUD;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,QAC0B,CAAC;AAMpE;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAIzE"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * The byte cap on R2 object transfer, and the arithmetic the routes need to
4
+ * enforce it before they buffer anything.
5
+ *
6
+ * One number, one home (Task 1695). The storage-broker MCP tools and the house
7
+ * ui routes both read the cap from here, so the process that rejects a too-large
8
+ * file and the process that would otherwise buffer it cannot drift apart.
9
+ *
10
+ * Deliberately dependency-free: the MCP imports this module directly, without
11
+ * the rest of the lib (cf-exec, registry, audit, remediate), which it has no use
12
+ * for. The ui route reads it through the barrel.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.R2_OBJECT_PUT_MAX_BODY_BYTES = exports.R2_OBJECT_MAX_BYTES = void 0;
16
+ exports.base64Ceiling = base64Ceiling;
17
+ exports.tooLargeMessage = tooLargeMessage;
18
+ /**
19
+ * The maximum size, in bytes, of an object moved through storage-r2-object-get
20
+ * or storage-r2-object-put.
21
+ *
22
+ * Set independently of the email plugin's OUTBOUND_ATTACHMENT_MAX_BYTES (25 MiB)
23
+ * even though the payload class is the same. The two exist for different reasons
24
+ * and must be free to move for different reasons: email's tracks an SMTP-side
25
+ * rejection threshold, this one tracks house-process memory. Do not couple them.
26
+ *
27
+ * Not bounded by R2. A single-part upload may be up to 5 GiB (doc-confirmed:
28
+ * developers.cloudflare.com/r2/platform/limits/), roughly fifty times this, and
29
+ * the v4 REST API states no separate object-size limit. This cap is the only
30
+ * bound on either transfer.
31
+ *
32
+ * The bound is not small: at the cap a get still leaves the house process
33
+ * holding the object and its base64 form, roughly 2.4x the object. That cost was
34
+ * weighed and accepted when the number was chosen.
35
+ */
36
+ exports.R2_OBJECT_MAX_BYTES = 100 * 1024 * 1024;
37
+ /** base64 emits 4 characters per 3 input bytes, padded up to the next quantum. */
38
+ function base64Ceiling(bytes) {
39
+ return 4 * Math.ceil(bytes / 3);
40
+ }
41
+ /**
42
+ * Room for the put envelope's bucket, key and JSON punctuation, so a legal
43
+ * at-cap put is not rejected by its own key. Far above any realistic bucket+key
44
+ * length and far below the cap, so it cannot smuggle a meaningfully larger
45
+ * object.
46
+ */
47
+ const PUT_ENVELOPE_ALLOWANCE_BYTES = 64 * 1024;
48
+ /**
49
+ * The largest JSON body /r2/object/put can legitimately carry.
50
+ *
51
+ * This bounds house memory; it does not adjudicate the last kilobyte. The exact
52
+ * decoded-byte rule lives in the MCP, which knows the source's size exactly.
53
+ */
54
+ exports.R2_OBJECT_PUT_MAX_BODY_BYTES = base64Ceiling(exports.R2_OBJECT_MAX_BYTES) + PUT_ENVELOPE_ALLOWANCE_BYTES;
55
+ function formatMiB(bytes) {
56
+ return (bytes / 1024 / 1024).toFixed(1);
57
+ }
58
+ /**
59
+ * The over-cap message, worded once so the MCP and the routes agree. Names the
60
+ * offending object and the limit, which is what makes the failure actionable
61
+ * rather than an opaque out-of-memory kill.
62
+ *
63
+ * Exact bytes accompany the MiB figures because MiB alone rounds: an object one
64
+ * byte over the cap reads "100.0 MiB, which exceeds the 100.0 MiB limit", which
65
+ * is a message that appears to contradict itself and invites the reader to treat
66
+ * the rejection as a bug. The bytes are what make it legible; the MiB is what
67
+ * makes it quick.
68
+ */
69
+ function tooLargeMessage(what, actualBytes) {
70
+ return `${what} is ${formatMiB(actualBytes)} MiB (${actualBytes} bytes), which exceeds the ${formatMiB(exports.R2_OBJECT_MAX_BYTES)} MiB (${exports.R2_OBJECT_MAX_BYTES} bytes) R2 object limit.`;
71
+ }
72
+ //# sourceMappingURL=object-limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-limits.js","sourceRoot":"","sources":["../src/object-limits.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAuBH,sCAEC;AAkCD,0CAIC;AA7DD;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,mBAAmB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAErD,kFAAkF;AAClF,SAAgB,aAAa,CAAC,KAAa;IACzC,OAAO,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,4BAA4B,GAAG,EAAE,GAAG,IAAI,CAAC;AAE/C;;;;;GAKG;AACU,QAAA,4BAA4B,GACvC,aAAa,CAAC,2BAAmB,CAAC,GAAG,4BAA4B,CAAC;AAEpE,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,CAAC,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,eAAe,CAAC,IAAY,EAAE,WAAmB;IAC/D,OAAO,GAAG,IAAI,OAAO,SAAS,CAAC,WAAW,CAAC,SAAS,WAAW,8BAA8B,SAAS,CACpG,2BAAmB,CACpB,SAAS,2BAAmB,0BAA0B,CAAC;AAC1D,CAAC"}
@@ -26,11 +26,18 @@ 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 { ok: response.ok, status: response.status, text: async () => response.body };
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
  }
@@ -41,13 +48,18 @@ function recordingFetch(response: { ok: boolean; status: number; body: string })
41
48
  function sequenceFetch(responses: Array<{ ok: boolean; status: number; body: string }>) {
42
49
  const calls: Array<{
43
50
  url: string;
44
- init: { method: string; headers: Record<string, string>; body?: string };
51
+ init: { method: string; headers: Record<string, string>; body?: string | Uint8Array };
45
52
  }> = [];
46
53
  const fetchFn: FetchFn = async (url, init) => {
47
54
  calls.push({ url, init });
48
55
  const r = responses[calls.length - 1];
49
56
  if (!r) throw new Error(`unexpected fetch call #${calls.length} to ${url}`);
50
- return { ok: r.ok, status: r.status, text: async () => r.body };
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
+ };
51
63
  };
52
64
  return { fetchFn, calls };
53
65
  }
@@ -99,7 +111,9 @@ test("d1Create creates the database via the D1 API with the house token, never w
99
111
  assert.equal(calls[0].init.method, "POST");
100
112
  assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
101
113
  assert.equal(calls[0].init.headers["Content-Type"], "application/json");
102
- assert.deepEqual(JSON.parse(calls[0].init.body ?? ""), { name: "gls-new" });
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" });
103
117
  });
104
118
 
105
119
  test("d1Create throws with the API error body on a non-2xx response", async () => {
@@ -338,3 +352,195 @@ test("makeHouseCfExec runs wrangler with the scoped token, not the minter", asyn
338
352
  await cf.d1List();
339
353
  assert.equal(sawToken, "scoped_tok", "wrangler saw the scoped token, not the minter");
340
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
+ });
@@ -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 r2List and d1Create are
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 (r2List) omit it.
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<{ ok: boolean; status: number; text(): Promise<string> }>;
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("/");
63
+ }
64
+
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)}`;
32
69
  }
33
70
 
34
- const defaultFetch: FetchFn = (url, init) => fetch(url, init);
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) => {
@@ -62,7 +104,7 @@ function parseJson(raw: string, context: string): unknown {
62
104
  async function cfApiJson(
63
105
  fetchFn: FetchFn,
64
106
  url: string,
65
- init: { method: string; headers: Record<string, string>; body?: string },
107
+ init: { method: string; headers: Record<string, string>; body?: string | Uint8Array },
66
108
  context: string,
67
109
  ): Promise<{ parsed: unknown; body: string }> {
68
110
  const res = await fetchFn(url, init);
@@ -182,6 +224,106 @@ export function makeCfExec(
182
224
  async r2Create(name) {
183
225
  await run("npx", ["wrangler", "r2", "bucket", "create", name], env);
184
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), {
301
+ method: "GET",
302
+ headers: { Authorization: `Bearer ${cred.apiToken}` },
303
+ });
304
+ if (!res.ok) {
305
+ throw new Error(
306
+ `storage-broker: r2 object get failed: ${res.status}\n${await res.text()}`,
307
+ );
308
+ }
309
+ return new Uint8Array(await res.arrayBuffer());
310
+ },
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
+ );
326
+ },
185
327
  };
186
328
  }
187
329