@rubytech/create-maxy-code 0.1.459 → 0.1.461

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 (189) hide show
  1. package/dist/__tests__/cron-heartbeat-registration.test.js +57 -28
  2. package/dist/__tests__/launchd-plist.test.js +26 -0
  3. package/dist/cron-registration.js +30 -8
  4. package/dist/index.js +50 -9
  5. package/dist/launchd-plist.js +1 -1
  6. package/dist/uninstall.js +9 -3
  7. package/package.json +1 -1
  8. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +116 -6
  9. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  10. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +20 -0
  11. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -1
  12. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
  13. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
  14. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
  15. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
  16. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +1 -0
  17. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  18. package/payload/platform/lib/storage-broker/dist/cf-exec.js +116 -48
  19. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  20. package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
  21. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
  22. package/payload/platform/lib/storage-broker/dist/index.js +1 -0
  23. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
  24. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +27 -0
  25. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -1
  26. package/payload/platform/lib/storage-broker/dist/object-limits.js +27 -7
  27. package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -1
  28. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
  29. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
  30. package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
  31. package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
  32. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +130 -7
  33. package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +30 -0
  34. package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
  35. package/payload/platform/lib/storage-broker/src/cf-exec.ts +129 -56
  36. package/payload/platform/lib/storage-broker/src/index.ts +1 -0
  37. package/payload/platform/lib/storage-broker/src/object-limits.ts +26 -6
  38. package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
  39. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -4
  40. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
  41. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +220 -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 +204 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
  51. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
  52. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  53. package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +125 -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.mjs +193 -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 +119 -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 +73 -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 +3 -3
  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__/detach.test.d.ts +2 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
  88. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
  89. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  90. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  91. package/payload/platform/plugins/outlook/mcp/dist/index.js +3 -2
  92. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  93. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
  94. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
  95. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
  96. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
  97. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
  98. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
  99. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
  100. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
  101. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +6 -1
  102. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
  103. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
  104. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +42 -22
  105. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  106. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +59 -23
  107. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  108. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
  109. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
  110. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
  111. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
  112. package/payload/platform/plugins/outlook/references/auth.md +1 -1
  113. package/payload/platform/plugins/outlook/references/graph-surfaces.md +7 -3
  114. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +1 -1
  115. package/payload/platform/plugins/storage-broker/PLUGIN.md +1 -1
  116. package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
  117. package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
  118. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
  119. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
  120. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
  121. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
  122. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
  123. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
  124. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  125. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +111 -12
  126. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  127. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
  128. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
  129. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
  130. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  131. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
  132. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  133. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  134. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
  135. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  136. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
  137. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  138. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
  139. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
  140. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +444 -0
  141. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +18 -2
  142. package/payload/platform/scripts/__tests__/logs-rotate.test.sh +256 -0
  143. package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
  144. package/payload/platform/scripts/logs-rotate.sh +204 -0
  145. package/payload/platform/scripts/resume-tunnel.sh +27 -0
  146. package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
  147. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  148. package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
  149. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  150. package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
  151. package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
  152. package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
  153. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  154. package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
  155. package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
  156. package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
  157. package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
  158. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
  159. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
  160. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
  161. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
  162. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
  163. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
  164. package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
  165. package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
  166. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
  167. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  168. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +36 -24
  169. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  170. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  171. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
  172. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  173. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
  174. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
  175. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
  176. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
  177. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
  178. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
  179. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
  180. package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
  181. package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
  182. package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
  183. package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
  184. package/payload/server/{chunk-Q6W4U6HL.js → chunk-JXWFVE5X.js} +83 -39
  185. package/payload/server/server.js +268 -73
  186. package/payload/server/{src-3I2RYZFB.js → src-4F37OHLK.js} +11 -1
  187. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
  188. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
  189. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
@@ -12,7 +12,7 @@
12
12
  * for. The ui route reads it through the barrel.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.R2_OBJECT_PUT_MAX_BODY_BYTES = exports.R2_OBJECT_MAX_BYTES = void 0;
15
+ exports.R2_OBJECT_PUT_MAX_BODY_BYTES = exports.R2_OBJECT_ENVELOPE_MAX_BODY_BYTES = exports.R2_OBJECT_MAX_BYTES = void 0;
16
16
  exports.base64Ceiling = base64Ceiling;
17
17
  exports.tooLargeMessage = tooLargeMessage;
18
18
  /**
@@ -39,19 +39,39 @@ function base64Ceiling(bytes) {
39
39
  return 4 * Math.ceil(bytes / 3);
40
40
  }
41
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.
42
+ * The largest JSON body an object route's metadata envelope can legitimately
43
+ * carry: a bucket name, one key or prefix, and JSON punctuation. No payload term.
44
+ *
45
+ * Used twice, because it answers one question — how large can an envelope
46
+ * legitimately get. `/r2/object/list`, `get` and `delete` carry an envelope and
47
+ * nothing else, so this is their whole body limit (Task 1698). `put` carries the
48
+ * same envelope plus base64 object bytes, so it adds this to the base64 ceiling
49
+ * below, which is the allowance that keeps a legal at-cap put from being rejected
50
+ * by its own key.
51
+ *
52
+ * Deliberately one constant rather than two that happen to match. This is the
53
+ * opposite ruling to the email cap above, for the opposite reason: that pair must
54
+ * stay independent because the numbers track different constraints and would drift
55
+ * for different reasons. Here the constraint is identical, so a change to either
56
+ * use must move both.
57
+ *
58
+ * Sized from confirmed R2 limits rather than assumed. An object key is at most
59
+ * 1,024 bytes (developers.cloudflare.com/r2/platform/limits/) and a bucket name is
60
+ * 3-63 characters (developers.cloudflare.com/r2/buckets/create-buckets/), so a
61
+ * real envelope is about 1.1 KB, or roughly 6 KiB if every key byte takes a
62
+ * six-character \uXXXX escape. This clears that by about ten times and sits about
63
+ * 2,100 times below put's body limit: far above anything a real caller sends, far
64
+ * below anything that threatens house memory. Nothing here depends on either
65
+ * figure being exact.
46
66
  */
47
- const PUT_ENVELOPE_ALLOWANCE_BYTES = 64 * 1024;
67
+ exports.R2_OBJECT_ENVELOPE_MAX_BODY_BYTES = 64 * 1024;
48
68
  /**
49
69
  * The largest JSON body /r2/object/put can legitimately carry.
50
70
  *
51
71
  * This bounds house memory; it does not adjudicate the last kilobyte. The exact
52
72
  * decoded-byte rule lives in the MCP, which knows the source's size exactly.
53
73
  */
54
- exports.R2_OBJECT_PUT_MAX_BODY_BYTES = base64Ceiling(exports.R2_OBJECT_MAX_BYTES) + PUT_ENVELOPE_ALLOWANCE_BYTES;
74
+ exports.R2_OBJECT_PUT_MAX_BODY_BYTES = base64Ceiling(exports.R2_OBJECT_MAX_BYTES) + exports.R2_OBJECT_ENVELOPE_MAX_BODY_BYTES;
55
75
  function formatMiB(bytes) {
56
76
  return (bytes / 1024 / 1024).toFixed(1);
57
77
  }
@@ -1 +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"}
1
+ {"version":3,"file":"object-limits.js","sourceRoot":"","sources":["../src/object-limits.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAuBH,sCAEC;AAsDD,0CAIC;AAjFD;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACU,QAAA,iCAAiC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3D;;;;;GAKG;AACU,QAAA,4BAA4B,GACvC,aAAa,CAAC,2BAAmB,CAAC,GAAG,yCAAiC,CAAC;AAEzE,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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The bounds on the request bodies the resource-level routes will buffer:
3
+ * /d1/create, /d1/query and /r2/create (Task 1702).
4
+ *
5
+ * Parallel to object-limits.ts, which bounds the object routes. Kept separate
6
+ * because the facts differ: an object envelope is dominated by R2's 1,024-byte
7
+ * object key limit, and these routes carry no key at all. Coupling them would tie
8
+ * a D1 database name to R2's key limit, so a revision to either would move a bound
9
+ * that has no reason to move.
10
+ *
11
+ * These are bounds on house memory. They do not adjudicate the last byte of a name
12
+ * or a statement — the same stance object-limits.ts takes, for the same reason. The
13
+ * house process holds the account-wide credential, so an out-of-memory kill there
14
+ * takes the brand server down, and on the target hardware that is a hard freeze
15
+ * that emits nothing.
16
+ *
17
+ * Deliberately dependency-free, like object-limits.ts: the ui route reads these
18
+ * through the barrel, and nothing here needs the rest of the lib.
19
+ */
20
+ /**
21
+ * Cloudflare's documented maximum SQL statement length.
22
+ *
23
+ * "Maximum SQL statement length | 100,000 bytes (100 KB)"
24
+ * https://developers.cloudflare.com/d1/platform/limits/
25
+ *
26
+ * Recorded because it is what governs a legitimate `sql`, and because
27
+ * D1_QUERY_MAX_BODY_BYTES below is only knowable as safe by way of it.
28
+ *
29
+ * A second, independent bound applies to the same string on the way out, and is
30
+ * recorded here so nobody rediscovers it as a bug: cf-exec's d1Query shells
31
+ * `execFile("npx", ["wrangler", "d1", "execute", ..., "--command", sql])`, so the
32
+ * decoded statement travels as a single argv element. Linux caps one such element
33
+ * at MAX_ARG_STRLEN = 131,072 bytes and fails execve with E2BIG above it
34
+ * (https://man7.org/linux/man-pages/man2/execve.2.html). That is a property of the
35
+ * spawn path, not of D1. It does not bind first: 100,000 < 131,072, so a statement
36
+ * D1 would accept always fits the spawn.
37
+ */
38
+ export declare const D1_MAX_SQL_STATEMENT_BYTES = 100000;
39
+ /**
40
+ * The worst-case expansion of a byte when it is serialized into a JSON string.
41
+ *
42
+ * A single ASCII control byte becomes the six characters \uXXXX. Multi-byte
43
+ * characters are cheaper per byte — a 3-byte UTF-8 character also becomes six
44
+ * characters (2x), and a non-BMP character becomes a 12-character surrogate pair
45
+ * for 4 bytes (3x) — so 6 is a true ceiling over bytes, reached by ASCII.
46
+ *
47
+ * This is why a body limit is not simply the statement limit: the cap bounds the
48
+ * wire body, and escaping moves the two apart.
49
+ */
50
+ export declare const JSON_STRING_WORST_CASE_EXPANSION = 6;
51
+ /**
52
+ * The largest JSON body /d1/create or /r2/create can legitimately carry: a single
53
+ * resource name and JSON punctuation. No payload term, and no key term.
54
+ *
55
+ * Sized on headroom rather than a tight derivation, because only one of the two
56
+ * names it bounds has a documented hard limit:
57
+ *
58
+ * - R2 bucket name: 3-63 characters, lowercase letters, digits and hyphens.
59
+ * https://developers.cloudflare.com/r2/buckets/create-buckets/
60
+ * - D1 database name: no documented limit. The API reference states only
61
+ * `name: string`, "D1 database name" — no maxLength, pattern or charset.
62
+ * https://developers.cloudflare.com/api/resources/d1/subresources/database/
63
+ * The get-started guide gives guidance, not a limit: names "should use a
64
+ * combination of ASCII characters, shorter than 32 characters".
65
+ *
66
+ * So a real body is under ~100 bytes and 64 KiB clears it by roughly 650x, while
67
+ * sitting ~2,100x below put's body limit: far above anything a real caller sends,
68
+ * far below anything that threatens house memory. Nothing here depends on either
69
+ * figure being exact. An undocumented D1 name limit cannot plausibly approach 64
70
+ * KiB, and a request body is the wrong place to discover that it had.
71
+ *
72
+ * Deliberately not R2_OBJECT_ENVELOPE_MAX_BODY_BYTES, which happens to hold the
73
+ * same value. That constant is governed by R2's object key limit; this one is not,
74
+ * and the two must be free to drift. Couple what is one fact; separate what merely
75
+ * matches today.
76
+ */
77
+ export declare const STORAGE_RESOURCE_NAME_MAX_BODY_BYTES: number;
78
+ /**
79
+ * The largest JSON body /d1/query can legitimately carry: a database name and a
80
+ * SQL statement.
81
+ *
82
+ * The envelope reasoning behind the object routes' bound does not govern this. A
83
+ * large but entirely legal migration or batch statement is not a 1,024-byte object
84
+ * key, so applying that constant here would reject correct queries.
85
+ *
86
+ * The bound is on the wire body, not the decoded statement, and JSON escaping moves
87
+ * those apart. The largest legal statement (D1_MAX_SQL_STATEMENT_BYTES) can present
88
+ * a wire body of up to
89
+ *
90
+ * JSON_STRING_WORST_CASE_EXPANSION * 100,000 + 65,536 = 665,536 bytes
91
+ *
92
+ * so capping at 100,000 would 413 a statement D1 would have accepted. 1 MiB clears
93
+ * the worst case by about 1.6x and sits ~133x below put's body limit.
94
+ *
95
+ * It is a flat number rather than that expression, for legibility at the call site.
96
+ * The arithmetic is not decoration: it is the only thing that makes 1 MiB knowable
97
+ * as safe, so resource-limits.test.ts asserts the relationship. If D1 ever raises
98
+ * its statement limit above 163,840 bytes, that test fails rather than this route
99
+ * quietly beginning to refuse legal SQL.
100
+ */
101
+ export declare const D1_QUERY_MAX_BODY_BYTES: number;
102
+ //# sourceMappingURL=resource-limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-limits.d.ts","sourceRoot":"","sources":["../src/resource-limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,SAAU,CAAC;AAElD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,oCAAoC,QAAY,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,uBAAuB,QAAc,CAAC"}
@@ -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"}
@@ -359,17 +359,23 @@ test("makeHouseCfExec runs wrangler with the scoped token, not the minter", asyn
359
359
  // proves it), and every failure keeps its body.
360
360
  // ---------------------------------------------------------------------------
361
361
 
362
- function objectPage(keys: string[], opts: { cursor?: string; truncated?: boolean } = {}) {
362
+ function objectPage(
363
+ keys: Array<string | { key: string; size: number }>,
364
+ opts: { cursor?: string; truncated?: boolean } = {},
365
+ ) {
363
366
  return JSON.stringify({
364
367
  success: true,
365
368
  errors: [],
366
369
  messages: [],
367
- result: keys.map((key) => ({
368
- key,
369
- size: 10,
370
- etag: "e1",
371
- last_modified: "2026-01-01T00:00:00Z",
372
- })),
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
+ }),
373
379
  result_info: { cursor: opts.cursor ?? "", is_truncated: opts.truncated ?? false },
374
380
  });
375
381
  }
@@ -544,3 +550,120 @@ test("r2ObjectList follows a cursor when is_truncated is absent, rather than tru
544
550
  assert.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]);
545
551
  assert.equal(calls.length, 2);
546
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
+ });
@@ -1,6 +1,7 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import {
4
+ R2_OBJECT_ENVELOPE_MAX_BODY_BYTES,
4
5
  R2_OBJECT_MAX_BYTES,
5
6
  R2_OBJECT_PUT_MAX_BODY_BYTES,
6
7
  base64Ceiling,
@@ -53,6 +54,35 @@ test("the envelope allowance is negligible against the cap", () => {
53
54
  assert.ok(allowance < R2_OBJECT_MAX_BYTES / 1000);
54
55
  });
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
+
56
86
  test("the over-cap message names the object and the limit", () => {
57
87
  const msg = tooLargeMessage("photo.jpg", 150 * 1024 * 1024);
58
88
  assert.match(msg, /photo\.jpg/);
@@ -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
+ });