Skip to content

Commit 63afac4

Browse files
authored
fix(cloudflare): require explicit R2 lifecycle replacement rules (#9225)
Require explicit R2 lifecycle rules before replacing configuration, retain intentional empty-array clears, align model input metadata, and regenerate docs.
1 parent 1f0dd8a commit 63afac4

6 files changed

Lines changed: 71 additions & 13 deletions

File tree

‎apps/docs/content/docs/integrations/cloudflare.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7744,7 +7744,7 @@ Replaces the object lifecycle rules for an R2 bucket. Rules match object-key pre
77447744
| `accountId` | string | Yes | Account id |
77457745
| `bucketName` | string | Yes | Bucket name |
77467746
| `apiKey` | string | Yes | Cloudflare API token |
7747-
| `rules` | json | No | JSON array. rules Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions. |
7747+
| `rules` | string | Yes | Complete JSON array of lifecycle rules. Read the current rules first and include every rule to retain. Use \[\] only to remove all lifecycle rules. Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions. |
77487748
| `jurisdiction` | string | No | Jurisdiction where objects in this bucket are guaranteed to be stored. |
77497749

77507750
#### Output

‎apps/sim/blocks/blocks/cloudflare.ts‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4300,14 +4300,13 @@ export const CloudflareBlock: BlockConfig = {
43004300
id: 'r2BucketLifecycleRules',
43014301
title: 'Rules',
43024302
type: 'long-input',
4303-
placeholder: 'Enter rules as JSON',
4303+
placeholder: 'Enter all lifecycle rules as JSON; [] removes all rules',
43044304
condition: { field: 'operation', value: ['update_r2_bucket_lifecycle'] },
4305-
required: false,
4306-
mode: 'advanced',
4305+
required: true,
43074306
wandConfig: {
43084307
enabled: true,
43094308
prompt:
4310-
'Generate the Cloudflare rules value. JSON array. rules Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions. Return ONLY the value.',
4309+
'Generate the complete Cloudflare lifecycle rules JSON array. Include every existing rule the user wants to retain. Use [] only when the user wants to remove all lifecycle rules. Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions. Return ONLY the value.',
43114310
placeholder: 'Describe the rules',
43124311
},
43134312
},
@@ -5300,9 +5299,9 @@ export const CloudflareBlock: BlockConfig = {
53005299
description: 'JSON array. rules Fields: allowed, exposeHeaders, id, maxAgeSeconds.',
53015300
},
53025301
r2BucketLifecycleRules: {
5303-
type: 'json',
5302+
type: 'string',
53045303
description:
5305-
'JSON array. rules Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions.',
5304+
'Complete JSON array of lifecycle rules. Read the current rules first and include every rule to retain. Use [] only to remove all lifecycle rules. Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions.',
53065305
},
53075306
rulesetRulePosition: {
53085307
type: 'json',

‎apps/sim/tools/cloudflare/operation-types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ export interface CloudflareUpdateR2BucketLifecycleParams {
327327
accountId: string
328328
bucketName: string
329329
apiKey: string
330-
rules?: string | Record<string, unknown> | unknown[]
330+
rules: string | unknown[]
331331
jurisdiction?: string
332332
}
333333

‎apps/sim/tools/cloudflare/request-boundary.test.ts‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,60 @@
11
import { describe, expect, it } from 'vitest'
22
import { createDnsRecordTool } from '@/tools/cloudflare/create_dns_record'
33
import { deleteDnsRecordTool } from '@/tools/cloudflare/delete_dns_record'
4+
import { updateR2BucketLifecycleTool } from '@/tools/cloudflare/update_r2_bucket_lifecycle'
45
import { updateRulesetRuleTool } from '@/tools/cloudflare/update_ruleset_rule'
56

67
describe('Cloudflare provider request boundaries', () => {
8+
it.each([undefined, null, '', ' '])(
9+
'rejects an R2 lifecycle replacement without explicit rules (%j)',
10+
(rules) => {
11+
const body = updateR2BucketLifecycleTool.request.body
12+
if (!body) throw new Error('Expected a request body')
13+
const wireInput = { apiKey: 'token', accountId: 'account', bucketName: 'bucket', rules }
14+
expect(() => Reflect.apply(body, undefined, [wireInput])).toThrow(/rules/)
15+
}
16+
)
17+
18+
it.each([{ rules: [] }, { rules: '[]' }])(
19+
'preserves an explicit R2 lifecycle clear (%j)',
20+
({ rules }) => {
21+
const body = updateR2BucketLifecycleTool.request.body
22+
if (!body) throw new Error('Expected a request body')
23+
expect(body({ apiKey: 'token', accountId: 'account', bucketName: 'bucket', rules })).toEqual({
24+
rules: [],
25+
})
26+
}
27+
)
28+
29+
it('preserves every explicitly supplied R2 lifecycle rule', () => {
30+
const body = updateR2BucketLifecycleTool.request.body
31+
if (!body) throw new Error('Expected a request body')
32+
const rules = [
33+
{
34+
id: 'expire-logs',
35+
enabled: true,
36+
conditions: { prefix: 'logs/' },
37+
deleteObjectsTransition: { condition: { type: 'Age', maxAge: 86400 } },
38+
},
39+
{
40+
id: 'retain-archive-transition',
41+
enabled: false,
42+
conditions: { prefix: 'archive/' },
43+
storageClassTransitions: [
44+
{ condition: { type: 'Age', maxAge: 2592000 }, storageClass: 'InfrequentAccess' },
45+
],
46+
},
47+
]
48+
expect(
49+
body({
50+
apiKey: 'token',
51+
accountId: 'account',
52+
bucketName: 'bucket',
53+
rules: JSON.stringify(rules),
54+
})
55+
).toEqual({ rules })
56+
})
57+
758
it('rejects a DNS deletion identifier that would address the collection instead', () => {
859
const url = deleteDnsRecordTool.request.url
960
if (typeof url !== 'function') throw new Error('Expected a parameterized URL')

‎apps/sim/tools/cloudflare/update_r2_bucket_lifecycle.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ export const updateR2BucketLifecycleTool: ToolConfig<
4040
description: 'Cloudflare API token',
4141
},
4242
rules: {
43-
type: 'json',
44-
required: false,
43+
type: 'string',
44+
required: true,
4545
visibility: 'user-or-llm',
4646
description:
47-
'JSON array. rules Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions.',
47+
'Complete JSON array of lifecycle rules. Read the current rules first and include every rule to retain. Use [] only to remove all lifecycle rules. Fields: abortMultipartUploadsTransition, conditions, deleteObjectsTransition, enabled, id, storageClassTransitions.',
4848
},
4949
jurisdiction: {
5050
type: 'string',
@@ -61,7 +61,15 @@ export const updateR2BucketLifecycleTool: ToolConfig<
6161
...cloudflareHeaders(params.apiKey),
6262
...(params.jurisdiction ? { 'cf-r2-jurisdiction': params.jurisdiction } : {}),
6363
}),
64-
body: (params) => ({ rules: parseJsonArrayParam(params.rules, 'rules') }),
64+
body: (params) => {
65+
const rules = parseJsonArrayParam(params.rules, 'rules')
66+
if (rules === undefined) {
67+
throw new Error(
68+
'rules is required; supply a JSON array or [] to remove all lifecycle rules'
69+
)
70+
}
71+
return { rules }
72+
},
6573
},
6674
transformResponse: async (response) => {
6775
const data = await readCloudflareResponse(response)

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)