@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.
- package/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +298 -3
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +79 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +165 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.js +4 -2
- package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +2 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +80 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js +92 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +334 -5
- package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +106 -0
- package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +221 -6
- package/payload/platform/lib/storage-broker/src/house-credential.ts +4 -2
- package/payload/platform/lib/storage-broker/src/index.ts +2 -0
- package/payload/platform/lib/storage-broker/src/object-limits.ts +95 -0
- package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -6
- package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
- package/payload/platform/plugins/cloudflare/bin/cf-token.sh +4 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +206 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +74 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
- package/payload/platform/plugins/cloudflare/references/api.md +1 -1
- package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +123 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +73 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +113 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +64 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
- package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +5 -5
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
- package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +12 -8
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +36 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +49 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +82 -13
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +8 -2
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +13 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
- package/payload/platform/plugins/outlook/references/auth.md +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +33 -6
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
- package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +13 -3
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +334 -0
- package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +13 -0
- package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
- package/payload/platform/scripts/logs-read.sh +63 -9
- package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +34 -23
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/server/{chunk-64FGYKNZ.js → chunk-JXWFVE5X.js} +148 -0
- package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +412 -37
- package/payload/server/{src-S7C4P6TT.js → src-4F37OHLK.js} +19 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
+
});
|