@rubytech/create-maxy-code 0.1.458 → 0.1.460

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 (218) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +298 -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 +79 -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/__tests__/resource-limits.test.d.ts +2 -0
  9. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
  10. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
  11. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
  12. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -1
  13. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  14. package/payload/platform/lib/storage-broker/dist/cf-exec.js +165 -0
  15. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  16. package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
  17. package/payload/platform/lib/storage-broker/dist/house-credential.js +4 -2
  18. package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
  19. package/payload/platform/lib/storage-broker/dist/index.d.ts +2 -0
  20. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
  21. package/payload/platform/lib/storage-broker/dist/index.js +2 -0
  22. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
  23. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +80 -0
  24. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
  25. package/payload/platform/lib/storage-broker/dist/object-limits.js +92 -0
  26. package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
  27. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
  28. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
  29. package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
  30. package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
  31. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +334 -5
  32. package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +106 -0
  33. package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
  34. package/payload/platform/lib/storage-broker/src/cf-exec.ts +221 -6
  35. package/payload/platform/lib/storage-broker/src/house-credential.ts +4 -2
  36. package/payload/platform/lib/storage-broker/src/index.ts +2 -0
  37. package/payload/platform/lib/storage-broker/src/object-limits.ts +95 -0
  38. package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
  39. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -6
  40. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
  41. package/payload/platform/plugins/cloudflare/bin/cf-token.sh +4 -3
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +206 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +74 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
  51. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  52. package/payload/platform/plugins/cloudflare/references/api.md +1 -1
  53. package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +123 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +73 -0
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +113 -0
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
  63. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
  64. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
  65. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
  66. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
  67. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
  68. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
  69. package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +64 -0
  70. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
  71. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
  72. package/payload/platform/plugins/docs/references/outlook-guide.md +5 -5
  73. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
  74. package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
  75. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
  76. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
  77. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
  78. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
  79. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  80. package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
  81. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
  88. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
  89. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
  90. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
  91. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
  92. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
  93. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
  94. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
  95. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
  96. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
  97. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
  98. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
  99. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
  100. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
  101. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  102. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  103. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
  104. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
  105. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
  106. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
  107. package/payload/platform/plugins/outlook/mcp/dist/index.js +12 -8
  108. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  109. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
  110. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
  111. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
  112. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
  113. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
  114. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
  115. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
  116. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
  117. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
  118. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
  119. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
  120. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
  121. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
  122. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
  123. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
  124. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
  125. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +36 -8
  126. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
  127. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
  128. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
  129. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
  130. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
  131. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
  132. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
  133. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +49 -4
  134. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  135. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +82 -13
  136. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  137. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +8 -2
  138. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  139. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +13 -4
  140. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  141. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
  142. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
  143. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
  144. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
  145. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  146. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
  147. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  148. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
  149. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
  150. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
  151. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
  152. package/payload/platform/plugins/outlook/references/auth.md +1 -1
  153. package/payload/platform/plugins/outlook/references/graph-surfaces.md +33 -6
  154. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
  155. package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
  156. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
  157. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
  158. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
  159. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
  160. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
  161. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
  162. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
  163. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
  164. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
  165. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
  166. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
  167. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
  168. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
  169. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
  170. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
  171. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
  172. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
  173. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
  174. package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
  175. package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
  176. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
  177. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
  178. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
  179. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
  180. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
  181. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
  182. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  183. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +13 -3
  184. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  185. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
  186. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
  187. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
  188. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  189. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
  190. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  191. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
  192. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  193. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
  194. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
  195. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +334 -0
  196. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +13 -0
  197. package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
  198. package/payload/platform/scripts/logs-read.sh +63 -9
  199. package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
  200. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  201. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
  202. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  203. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
  204. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
  205. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
  206. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
  207. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
  208. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  209. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +34 -23
  210. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  211. package/payload/server/{chunk-64FGYKNZ.js → chunk-JXWFVE5X.js} +148 -0
  212. package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
  213. package/payload/server/maxy-edge.js +1 -1
  214. package/payload/server/server.js +412 -37
  215. package/payload/server/{src-S7C4P6TT.js → src-4F37OHLK.js} +19 -1
  216. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
  217. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
  218. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * The bounds on the request bodies the resource-level routes will buffer:
4
+ * /d1/create, /d1/query and /r2/create (Task 1702).
5
+ *
6
+ * Parallel to object-limits.ts, which bounds the object routes. Kept separate
7
+ * because the facts differ: an object envelope is dominated by R2's 1,024-byte
8
+ * object key limit, and these routes carry no key at all. Coupling them would tie
9
+ * a D1 database name to R2's key limit, so a revision to either would move a bound
10
+ * that has no reason to move.
11
+ *
12
+ * These are bounds on house memory. They do not adjudicate the last byte of a name
13
+ * or a statement — the same stance object-limits.ts takes, for the same reason. The
14
+ * house process holds the account-wide credential, so an out-of-memory kill there
15
+ * takes the brand server down, and on the target hardware that is a hard freeze
16
+ * that emits nothing.
17
+ *
18
+ * Deliberately dependency-free, like object-limits.ts: the ui route reads these
19
+ * through the barrel, and nothing here needs the rest of the lib.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.D1_QUERY_MAX_BODY_BYTES = exports.STORAGE_RESOURCE_NAME_MAX_BODY_BYTES = exports.JSON_STRING_WORST_CASE_EXPANSION = exports.D1_MAX_SQL_STATEMENT_BYTES = void 0;
23
+ /**
24
+ * Cloudflare's documented maximum SQL statement length.
25
+ *
26
+ * "Maximum SQL statement length | 100,000 bytes (100 KB)"
27
+ * https://developers.cloudflare.com/d1/platform/limits/
28
+ *
29
+ * Recorded because it is what governs a legitimate `sql`, and because
30
+ * D1_QUERY_MAX_BODY_BYTES below is only knowable as safe by way of it.
31
+ *
32
+ * A second, independent bound applies to the same string on the way out, and is
33
+ * recorded here so nobody rediscovers it as a bug: cf-exec's d1Query shells
34
+ * `execFile("npx", ["wrangler", "d1", "execute", ..., "--command", sql])`, so the
35
+ * decoded statement travels as a single argv element. Linux caps one such element
36
+ * at MAX_ARG_STRLEN = 131,072 bytes and fails execve with E2BIG above it
37
+ * (https://man7.org/linux/man-pages/man2/execve.2.html). That is a property of the
38
+ * spawn path, not of D1. It does not bind first: 100,000 < 131,072, so a statement
39
+ * D1 would accept always fits the spawn.
40
+ */
41
+ exports.D1_MAX_SQL_STATEMENT_BYTES = 100_000;
42
+ /**
43
+ * The worst-case expansion of a byte when it is serialized into a JSON string.
44
+ *
45
+ * A single ASCII control byte becomes the six characters \uXXXX. Multi-byte
46
+ * characters are cheaper per byte — a 3-byte UTF-8 character also becomes six
47
+ * characters (2x), and a non-BMP character becomes a 12-character surrogate pair
48
+ * for 4 bytes (3x) — so 6 is a true ceiling over bytes, reached by ASCII.
49
+ *
50
+ * This is why a body limit is not simply the statement limit: the cap bounds the
51
+ * wire body, and escaping moves the two apart.
52
+ */
53
+ exports.JSON_STRING_WORST_CASE_EXPANSION = 6;
54
+ /**
55
+ * The largest JSON body /d1/create or /r2/create can legitimately carry: a single
56
+ * resource name and JSON punctuation. No payload term, and no key term.
57
+ *
58
+ * Sized on headroom rather than a tight derivation, because only one of the two
59
+ * names it bounds has a documented hard limit:
60
+ *
61
+ * - R2 bucket name: 3-63 characters, lowercase letters, digits and hyphens.
62
+ * https://developers.cloudflare.com/r2/buckets/create-buckets/
63
+ * - D1 database name: no documented limit. The API reference states only
64
+ * `name: string`, "D1 database name" — no maxLength, pattern or charset.
65
+ * https://developers.cloudflare.com/api/resources/d1/subresources/database/
66
+ * The get-started guide gives guidance, not a limit: names "should use a
67
+ * combination of ASCII characters, shorter than 32 characters".
68
+ *
69
+ * So a real body is under ~100 bytes and 64 KiB clears it by roughly 650x, while
70
+ * sitting ~2,100x below put's body limit: far above anything a real caller sends,
71
+ * far below anything that threatens house memory. Nothing here depends on either
72
+ * figure being exact. An undocumented D1 name limit cannot plausibly approach 64
73
+ * KiB, and a request body is the wrong place to discover that it had.
74
+ *
75
+ * Deliberately not R2_OBJECT_ENVELOPE_MAX_BODY_BYTES, which happens to hold the
76
+ * same value. That constant is governed by R2's object key limit; this one is not,
77
+ * and the two must be free to drift. Couple what is one fact; separate what merely
78
+ * matches today.
79
+ */
80
+ exports.STORAGE_RESOURCE_NAME_MAX_BODY_BYTES = 64 * 1024;
81
+ /**
82
+ * The largest JSON body /d1/query can legitimately carry: a database name and a
83
+ * SQL statement.
84
+ *
85
+ * The envelope reasoning behind the object routes' bound does not govern this. A
86
+ * large but entirely legal migration or batch statement is not a 1,024-byte object
87
+ * key, so applying that constant here would reject correct queries.
88
+ *
89
+ * The bound is on the wire body, not the decoded statement, and JSON escaping moves
90
+ * those apart. The largest legal statement (D1_MAX_SQL_STATEMENT_BYTES) can present
91
+ * a wire body of up to
92
+ *
93
+ * JSON_STRING_WORST_CASE_EXPANSION * 100,000 + 65,536 = 665,536 bytes
94
+ *
95
+ * so capping at 100,000 would 413 a statement D1 would have accepted. 1 MiB clears
96
+ * the worst case by about 1.6x and sits ~133x below put's body limit.
97
+ *
98
+ * It is a flat number rather than that expression, for legibility at the call site.
99
+ * The arithmetic is not decoration: it is the only thing that makes 1 MiB knowable
100
+ * as safe, so resource-limits.test.ts asserts the relationship. If D1 ever raises
101
+ * its statement limit above 163,840 bytes, that test fails rather than this route
102
+ * quietly beginning to refuse legal SQL.
103
+ */
104
+ exports.D1_QUERY_MAX_BODY_BYTES = 1024 * 1024;
105
+ //# sourceMappingURL=resource-limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-limits.js","sourceRoot":"","sources":["../src/resource-limits.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,0BAA0B,GAAG,OAAO,CAAC;AAElD;;;;;;;;;;GAUG;AACU,QAAA,gCAAgC,GAAG,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACU,QAAA,oCAAoC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACU,QAAA,uBAAuB,GAAG,IAAI,GAAG,IAAI,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,318 @@ 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(
363
+ keys: Array<string | { key: string; size: number }>,
364
+ opts: { cursor?: string; truncated?: boolean } = {},
365
+ ) {
366
+ return JSON.stringify({
367
+ success: true,
368
+ errors: [],
369
+ messages: [],
370
+ result: keys.map((k) => {
371
+ const entry = typeof k === "string" ? { key: k, size: 10 } : k;
372
+ return {
373
+ key: entry.key,
374
+ size: entry.size,
375
+ etag: "e1",
376
+ last_modified: "2026-01-01T00:00:00Z",
377
+ };
378
+ }),
379
+ result_info: { cursor: opts.cursor ?? "", is_truncated: opts.truncated ?? false },
380
+ });
381
+ }
382
+
383
+ test("r2ObjectList returns a single page and never shells wrangler", async () => {
384
+ const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage(["a.jpg"]) });
385
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
386
+ assert.deepEqual(await cf.r2ObjectList("acc-a-portal"), [
387
+ { key: "a.jpg", size: 10, etag: "e1", lastModified: "2026-01-01T00:00:00Z" },
388
+ ]);
389
+ assert.equal(calls.length, 1);
390
+ assert.equal(
391
+ calls[0].url,
392
+ "https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets/acc-a-portal/objects",
393
+ );
394
+ assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
395
+ });
396
+
397
+ test("r2ObjectList never sends per_page", async () => {
398
+ const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) });
399
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
400
+ await cf.r2ObjectList("b");
401
+ assert.equal(calls[0].url.includes("per_page"), false);
402
+ });
403
+
404
+ test("r2ObjectList passes prefix through, encoded", async () => {
405
+ const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) });
406
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
407
+ await cf.r2ObjectList("b", "person a/");
408
+ assert.ok(calls[0].url.endsWith("/objects?prefix=person+a%2F"), calls[0].url);
409
+ });
410
+
411
+ test("r2ObjectList follows the cursor to exhaustion", async () => {
412
+ const { fetchFn, calls } = sequenceFetch([
413
+ { ok: true, status: 200, body: objectPage(["a.jpg"], { cursor: "c1", truncated: true }) },
414
+ { ok: true, status: 200, body: objectPage(["b.jpg"]) },
415
+ ]);
416
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
417
+ assert.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]);
418
+ assert.equal(calls.length, 2);
419
+ assert.ok(calls[1].url.includes("cursor=c1"));
420
+ });
421
+
422
+ // is_truncated is the authoritative continuation signal, not the presence of a
423
+ // cursor: a last page may still carry one.
424
+ test("r2ObjectList stops when is_truncated is false even if a cursor is present", async () => {
425
+ const { fetchFn, calls } = sequenceFetch([
426
+ { ok: true, status: 200, body: objectPage(["a.jpg"], { cursor: "c1", truncated: false }) },
427
+ ]);
428
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
429
+ await cf.r2ObjectList("b");
430
+ assert.equal(calls.length, 1);
431
+ });
432
+
433
+ // The response shape is doc-confirmed but never measured against a live account.
434
+ // If it is not what we understand, that must be loud: a partial list is exactly
435
+ // the bug this loop exists to prevent.
436
+ test("r2ObjectList throws when truncated but no cursor is given, never a partial list", async () => {
437
+ const { fetchFn } = recordingFetch({
438
+ ok: true,
439
+ status: 200,
440
+ body: objectPage(["a.jpg"], { truncated: true }),
441
+ });
442
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
443
+ await assert.rejects(() => cf.r2ObjectList("b"), /truncated but gave no pagination cursor/);
444
+ });
445
+
446
+ test("r2ObjectList throws on a repeated cursor", async () => {
447
+ const { fetchFn } = sequenceFetch([
448
+ { ok: true, status: 200, body: objectPage(["a"], { cursor: "c1", truncated: true }) },
449
+ { ok: true, status: 200, body: objectPage(["b"], { cursor: "c1", truncated: true }) },
450
+ ]);
451
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
452
+ await assert.rejects(() => cf.r2ObjectList("b"), /repeated pagination cursor/);
453
+ });
454
+
455
+ test("r2ObjectList retains the body on a non-2xx", async () => {
456
+ const { fetchFn } = recordingFetch({ ok: false, status: 403, body: '{"errors":[{"code":10000}]}' });
457
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
458
+ await assert.rejects(() => cf.r2ObjectList("b"), /10000/);
459
+ });
460
+
461
+ test("r2ObjectGet returns exact bytes and encodes the key segment-wise", async () => {
462
+ // Binary that is not valid UTF-8: proves the body never round-trips through
463
+ // text(), which would corrupt it.
464
+ const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff]);
465
+ const calls: string[] = [];
466
+ const fetchFn: FetchFn = async (url) => {
467
+ calls.push(url);
468
+ return {
469
+ ok: true,
470
+ status: 200,
471
+ text: async () => "unused",
472
+ arrayBuffer: async () => bytes.buffer.slice(0) as ArrayBuffer,
473
+ };
474
+ };
475
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
476
+ const got = await cf.r2ObjectGet("b", "person a/photo 1.jpg");
477
+ assert.deepEqual(Array.from(got), Array.from(bytes));
478
+ // Separators stay separators; spaces are escaped.
479
+ assert.ok(calls[0].endsWith("/objects/person%20a/photo%201.jpg"), calls[0]);
480
+ });
481
+
482
+ test("r2ObjectGet retains the body on a non-2xx", async () => {
483
+ const { fetchFn } = recordingFetch({ ok: false, status: 404, body: "no such key" });
484
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
485
+ await assert.rejects(() => cf.r2ObjectGet("b", "k"), /no such key/);
486
+ });
487
+
488
+ test("r2ObjectPut sends the bytes as the request body", async () => {
489
+ const { fetchFn, calls } = recordingFetch({
490
+ ok: true,
491
+ status: 200,
492
+ body: JSON.stringify({ success: true, errors: [], messages: [], result: {} }),
493
+ });
494
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
495
+ const bytes = new Uint8Array([1, 2, 3]);
496
+ await cf.r2ObjectPut("b", "k.bin", bytes);
497
+ assert.equal(calls[0].init.method, "PUT");
498
+ assert.deepEqual(calls[0].init.body, bytes);
499
+ assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
500
+ });
501
+
502
+ test("r2ObjectPut retains the body on a non-success envelope", async () => {
503
+ const { fetchFn } = recordingFetch({
504
+ ok: true,
505
+ status: 200,
506
+ body: '{"success":false,"errors":[{"code":10001}]}',
507
+ });
508
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
509
+ await assert.rejects(() => cf.r2ObjectPut("b", "k", new Uint8Array([1])), /10001/);
510
+ });
511
+
512
+ test("r2ObjectDelete issues a DELETE", async () => {
513
+ const { fetchFn, calls } = recordingFetch({
514
+ ok: true,
515
+ status: 200,
516
+ body: JSON.stringify({ success: true, errors: [], messages: [], result: {} }),
517
+ });
518
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
519
+ await cf.r2ObjectDelete("b", "k");
520
+ assert.equal(calls[0].init.method, "DELETE");
521
+ assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x");
522
+ });
523
+
524
+ test("r2ObjectDelete retains the body on a non-2xx", async () => {
525
+ const { fetchFn } = recordingFetch({ ok: false, status: 500, body: "boom" });
526
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
527
+ await assert.rejects(() => cf.r2ObjectDelete("b", "k"), /boom/);
528
+ });
529
+
530
+ // Code review — is_truncated is itself an unmeasured fact (no live credential
531
+ // exists to confirm the envelope). Driving the loop solely on it means a
532
+ // response that omits it while still paginating truncates SILENTLY, which is
533
+ // the exact failure this loop exists to prevent, and it would also make
534
+ // r2ObjectList disagree with r2List directly above it, which drives the same
535
+ // envelope on cursor presence alone.
536
+ test("r2ObjectList follows a cursor when is_truncated is absent, rather than truncating silently", async () => {
537
+ const pageWithoutTruncatedFlag = (keys: string[], cursor: string | null) =>
538
+ JSON.stringify({
539
+ success: true,
540
+ errors: [],
541
+ messages: [],
542
+ result: keys.map((key) => ({ key, size: 1, etag: "e", last_modified: "t" })),
543
+ result_info: cursor === null ? {} : { cursor },
544
+ });
545
+ const { fetchFn, calls } = sequenceFetch([
546
+ { ok: true, status: 200, body: pageWithoutTruncatedFlag(["a.jpg"], "c1") },
547
+ { ok: true, status: 200, body: pageWithoutTruncatedFlag(["b.jpg"], null) },
548
+ ]);
549
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
550
+ assert.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]);
551
+ assert.equal(calls.length, 2);
552
+ });
553
+
554
+ // --- r2ObjectFind (Task 1697) ---------------------------------------------
555
+ //
556
+ // Sizing a get needs one record, not the prefix. These tests pin the two
557
+ // properties that make the early exit safe: it matches the exact key, and it
558
+ // stops at the page carrying it.
559
+
560
+ test("r2ObjectFind returns the exact key when a prefix neighbour is listed first", async () => {
561
+ // Not hypothetical. Measured 2026-07-16 against the live v4 API: prefix=
562
+ // a/photo.jpg really does return a/photo.jpg.bak BEFORE a/photo.jpg, because
563
+ // the listing is not in ascending UTF-8 binary order. Taking the first element
564
+ // would size the neighbour, which either rejects a legal get or admits an
565
+ // oversized one.
566
+ const { fetchFn } = recordingFetch({
567
+ ok: true,
568
+ status: 200,
569
+ body: objectPage([
570
+ { key: "a/photo.jpg.bak", size: 999 },
571
+ { key: "a/photo.jpg", size: 3 },
572
+ ]),
573
+ });
574
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
575
+ const found = await cf.r2ObjectFind("b", "a/photo.jpg");
576
+ assert.equal(found?.key, "a/photo.jpg");
577
+ assert.equal(found?.size, 3);
578
+ });
579
+
580
+ test("r2ObjectFind sends the key as the prefix, encoded", async () => {
581
+ const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) });
582
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
583
+ await cf.r2ObjectFind("b", "person a/x.jpg");
584
+ assert.ok(calls[0].url.endsWith("/objects?prefix=person+a%2Fx.jpg"), calls[0].url);
585
+ });
586
+
587
+ test("r2ObjectFind stops at the page carrying the match and fetches no further page", async () => {
588
+ // The page says truncated with a cursor, so r2ObjectList would fetch page 2.
589
+ // sequenceFetch throws on an unexpected call, so a second fetch fails here.
590
+ const { fetchFn, calls } = sequenceFetch([
591
+ {
592
+ ok: true,
593
+ status: 200,
594
+ body: objectPage([{ key: "k", size: 7 }], { cursor: "c1", truncated: true }),
595
+ },
596
+ ]);
597
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
598
+ const found = await cf.r2ObjectFind("b", "k");
599
+ assert.equal(found?.size, 7);
600
+ assert.equal(calls.length, 1);
601
+ });
602
+
603
+ test("r2ObjectFind finds a match on a later page, never assuming position", async () => {
604
+ const { fetchFn, calls } = sequenceFetch([
605
+ { ok: true, status: 200, body: objectPage(["x.jpg"], { cursor: "c1", truncated: true }) },
606
+ { ok: true, status: 200, body: objectPage([{ key: "k", size: 5 }], { truncated: false }) },
607
+ ]);
608
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
609
+ const found = await cf.r2ObjectFind("b", "k");
610
+ assert.equal(found?.size, 5);
611
+ assert.equal(calls.length, 2);
612
+ });
613
+
614
+ test("r2ObjectFind returns null when the key is absent after exhausting pages", async () => {
615
+ const { fetchFn, calls } = sequenceFetch([
616
+ { ok: true, status: 200, body: objectPage(["k.bak"], { cursor: "c1", truncated: true }) },
617
+ { ok: true, status: 200, body: objectPage(["k.zzz"], { truncated: false }) },
618
+ ]);
619
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
620
+ assert.equal(await cf.r2ObjectFind("b", "k"), null);
621
+ assert.equal(calls.length, 2);
622
+ });
623
+
624
+ test("r2ObjectFind throws on a repeated cursor instead of looping forever", async () => {
625
+ const { fetchFn } = sequenceFetch([
626
+ { ok: true, status: 200, body: objectPage(["x"], { cursor: "c1", truncated: true }) },
627
+ { ok: true, status: 200, body: objectPage(["y"], { cursor: "c1", truncated: true }) },
628
+ ]);
629
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
630
+ await assert.rejects(() => cf.r2ObjectFind("b", "k"), /repeated pagination cursor/);
631
+ });
632
+
633
+ test("r2ObjectFind throws when truncated but no cursor is given", async () => {
634
+ const { fetchFn } = recordingFetch({
635
+ ok: true,
636
+ status: 200,
637
+ body: objectPage(["x"], { cursor: "", truncated: true }),
638
+ });
639
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
640
+ await assert.rejects(() => cf.r2ObjectFind("b", "k"), /truncated but gave no pagination cursor/);
641
+ });
642
+
643
+ test("r2ObjectFind retains the body on a non-2xx", async () => {
644
+ const { fetchFn } = recordingFetch({ ok: false, status: 403, body: '{"errors":[{"code":10000}]}' });
645
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
646
+ await assert.rejects(() => cf.r2ObjectFind("b", "k"), /10000/);
647
+ });
648
+
649
+ // Code review — the early exit changes observable behaviour on an anomalous
650
+ // page, and an unpinned behaviour change is how a regression starts. Pinned as a
651
+ // decision rather than left implicit.
652
+ //
653
+ // A page that reports truncated with no cursor is an anomaly r2ObjectList throws
654
+ // on, because a partial listing would corrupt the audit. r2ObjectFind returns at
655
+ // the match and never reaches that check, so the same response yields the size
656
+ // instead of an error. That is correct: the size is Cloudflare's own record for
657
+ // the exact key, the cap is enforced on it, and the anomaly concerns pages the
658
+ // sizing does not need. The audit's behaviour on the same response is unchanged
659
+ // and is pinned by "r2ObjectList throws when truncated but no cursor is given".
660
+ test("r2ObjectFind returns a match found before an anomalous page's guard is reached", async () => {
661
+ const { fetchFn } = recordingFetch({
662
+ ok: true,
663
+ status: 200,
664
+ body: objectPage([{ key: "k", size: 4 }], { cursor: "", truncated: true }),
665
+ });
666
+ const cf = makeCfExec(cred, throwingRun, fetchFn);
667
+ const found = await cf.r2ObjectFind("b", "k");
668
+ assert.equal(found?.size, 4);
669
+ });
@@ -0,0 +1,106 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ R2_OBJECT_ENVELOPE_MAX_BODY_BYTES,
5
+ R2_OBJECT_MAX_BYTES,
6
+ R2_OBJECT_PUT_MAX_BODY_BYTES,
7
+ base64Ceiling,
8
+ tooLargeMessage,
9
+ } from "../object-limits.js";
10
+
11
+ test("the cap is 100 MiB", () => {
12
+ assert.equal(R2_OBJECT_MAX_BYTES, 100 * 1024 * 1024);
13
+ });
14
+
15
+ // The cap is set independently of email's 25 MiB. This pins that they are not
16
+ // the same number, so a future edit that "unifies" them fails here and has to
17
+ // read the design note rather than silently coupling two unrelated limits.
18
+ test("the cap is not email's attachment cap", () => {
19
+ assert.notEqual(R2_OBJECT_MAX_BYTES, 25 * 1024 * 1024);
20
+ });
21
+
22
+ test("base64Ceiling matches base64's 4-out-per-3-in expansion, rounded up", () => {
23
+ assert.equal(base64Ceiling(0), 0);
24
+ assert.equal(base64Ceiling(1), 4);
25
+ assert.equal(base64Ceiling(3), 4);
26
+ assert.equal(base64Ceiling(4), 8);
27
+ assert.equal(base64Ceiling(6), 8);
28
+ });
29
+
30
+ // The ceiling must never sit below what a legal at-cap object actually encodes
31
+ // to, or a legitimate put would be rejected by its own encoding.
32
+ test("base64Ceiling is never below Node's own base64 length", () => {
33
+ for (const n of [0, 1, 2, 3, 4, 5, 100, 1023, 4096]) {
34
+ const encoded = Buffer.alloc(n).toString("base64").length;
35
+ assert.ok(
36
+ base64Ceiling(n) >= encoded,
37
+ `base64Ceiling(${n}) = ${base64Ceiling(n)} < ${encoded}`,
38
+ );
39
+ }
40
+ });
41
+
42
+ test("the put body ceiling admits a cap-sized object plus an envelope allowance", () => {
43
+ assert.equal(
44
+ R2_OBJECT_PUT_MAX_BODY_BYTES,
45
+ base64Ceiling(R2_OBJECT_MAX_BYTES) + 64 * 1024,
46
+ );
47
+ assert.ok(R2_OBJECT_PUT_MAX_BODY_BYTES > base64Ceiling(R2_OBJECT_MAX_BYTES));
48
+ });
49
+
50
+ // The allowance is for a key and JSON punctuation, not for smuggling a bigger
51
+ // object: it must stay far below the cap it guards.
52
+ test("the envelope allowance is negligible against the cap", () => {
53
+ const allowance = R2_OBJECT_PUT_MAX_BODY_BYTES - base64Ceiling(R2_OBJECT_MAX_BYTES);
54
+ assert.ok(allowance < R2_OBJECT_MAX_BYTES / 1000);
55
+ });
56
+
57
+ test("the envelope limit is 64 KiB", () => {
58
+ assert.equal(R2_OBJECT_ENVELOPE_MAX_BODY_BYTES, 64 * 1024);
59
+ });
60
+
61
+ // The sizing rationale, pinned rather than left in prose. An object key is at
62
+ // most 1,024 bytes and a bucket name at most 63 characters, so even a key whose
63
+ // every byte takes a six-character \uXXXX escape leaves an order of magnitude of
64
+ // headroom. A future edit that shrinks this toward a real envelope fails here.
65
+ test("the envelope limit clears a worst-case escaped envelope with headroom", () => {
66
+ const worstCase = 63 + 1024 * 6 + 64; // bucket + fully escaped key + punctuation
67
+ assert.ok(
68
+ R2_OBJECT_ENVELOPE_MAX_BODY_BYTES > worstCase * 5,
69
+ `${R2_OBJECT_ENVELOPE_MAX_BODY_BYTES} leaves too little headroom over ${worstCase}`,
70
+ );
71
+ });
72
+
73
+ // The counterpart to "the cap is not email's attachment cap" above, and the
74
+ // opposite ruling for the opposite reason. That pair must stay apart because the
75
+ // two numbers track different constraints and would drift for different reasons.
76
+ // These two are one fact — how large can an envelope legitimately get — asked once
77
+ // by put's allowance and once by the metadata routes' whole body, so they must
78
+ // move together. This pins the composition; the test above pins the value.
79
+ test("put's body ceiling is composed of the same envelope constant", () => {
80
+ assert.equal(
81
+ R2_OBJECT_PUT_MAX_BODY_BYTES,
82
+ base64Ceiling(R2_OBJECT_MAX_BYTES) + R2_OBJECT_ENVELOPE_MAX_BODY_BYTES,
83
+ );
84
+ });
85
+
86
+ test("the over-cap message names the object and the limit", () => {
87
+ const msg = tooLargeMessage("photo.jpg", 150 * 1024 * 1024);
88
+ assert.match(msg, /photo\.jpg/);
89
+ assert.match(msg, /150\.0 MiB/);
90
+ assert.match(msg, /100\.0 MiB/);
91
+ });
92
+
93
+ // Code review — MiB alone rounds, so an object one byte over the cap rendered as
94
+ // "100.0 MiB, which exceeds the 100.0 MiB limit": a message that contradicts
95
+ // itself and reads as a bug rather than a rejection. The exact bytes are what
96
+ // disambiguate it, so they are pinned rather than left to formatting taste.
97
+ test("the message stays legible for an object one byte over the cap", () => {
98
+ const msg = tooLargeMessage("photo.jpg", R2_OBJECT_MAX_BYTES + 1);
99
+ assert.match(msg, /104857601 bytes/);
100
+ assert.match(msg, /104857600 bytes/);
101
+ assert.notEqual(
102
+ msg.match(/100\.0 MiB \(104857601 bytes\)/),
103
+ null,
104
+ "the actual size must carry its exact byte count",
105
+ );
106
+ });
@@ -0,0 +1,49 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ D1_MAX_SQL_STATEMENT_BYTES,
5
+ D1_QUERY_MAX_BODY_BYTES,
6
+ JSON_STRING_WORST_CASE_EXPANSION,
7
+ STORAGE_RESOURCE_NAME_MAX_BODY_BYTES,
8
+ } from "../resource-limits.js";
9
+
10
+ test("the recorded D1 statement limit is Cloudflare's documented 100,000 bytes", () => {
11
+ assert.equal(D1_MAX_SQL_STATEMENT_BYTES, 100_000);
12
+ });
13
+
14
+ test("a resource-name body is bounded at 64 KiB", () => {
15
+ assert.equal(STORAGE_RESOURCE_NAME_MAX_BODY_BYTES, 64 * 1024);
16
+ });
17
+
18
+ test("a D1 query body is bounded at 1 MiB", () => {
19
+ assert.equal(D1_QUERY_MAX_BODY_BYTES, 1024 * 1024);
20
+ });
21
+
22
+ // The guard. D1_QUERY_MAX_BODY_BYTES is a flat number rather than a computed
23
+ // expression, so nothing but this test holds it to the arithmetic that makes it
24
+ // safe. If D1 ever raises its statement limit above 163,840 bytes, the worst-case
25
+ // wire form of a legal statement exceeds 1 MiB and the route would begin rejecting
26
+ // correct queries with nothing to signal why. This fails first instead.
27
+ test("the D1 query body limit clears the worst-case wire form of a legal statement", () => {
28
+ const worstCaseLegalBody =
29
+ JSON_STRING_WORST_CASE_EXPANSION * D1_MAX_SQL_STATEMENT_BYTES +
30
+ STORAGE_RESOURCE_NAME_MAX_BODY_BYTES;
31
+ assert.ok(
32
+ D1_QUERY_MAX_BODY_BYTES > worstCaseLegalBody,
33
+ `D1_QUERY_MAX_BODY_BYTES (${D1_QUERY_MAX_BODY_BYTES}) must exceed the largest legal wire body (${worstCaseLegalBody}), or legal queries are rejected`,
34
+ );
35
+ });
36
+
37
+ // The escape ceiling is the reason the bound is not simply D1's 100,000. Pin the
38
+ // factor against the runtime rather than restating the literal: a single ASCII
39
+ // control byte serializes to the six characters \uXXXX, which is the worst ratio a
40
+ // JSON string can reach over bytes.
41
+ test("the JSON worst-case expansion is the six-character \\uXXXX escape", () => {
42
+ assert.equal(JSON_STRING_WORST_CASE_EXPANSION, 6);
43
+ // Written as an escape rather than a literal control byte: a raw 0x01 in source
44
+ // is invisible in most editors and diffs.
45
+ const oneControlByte = "\u0001";
46
+ assert.equal(Buffer.byteLength(oneControlByte), 1);
47
+ // minus the two quotes JSON.stringify wraps the string in
48
+ assert.equal(JSON.stringify(oneControlByte).length - 2, JSON_STRING_WORST_CASE_EXPANSION);
49
+ });