Skip to content

Commit 39fcff4

Browse files
committed
Address Cloudflare adapter review feedback
1 parent 2789741 commit 39fcff4

3 files changed

Lines changed: 73 additions & 16 deletions

File tree

packages/cloud/cloudflare/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Provides the Cloudflare (R2 / D1 / Queues / Tunnels) cloud provider adapter for
1111

1212
Set `resourceType` to one of `r2-bucket`, `d1-database`, `queue`, or `tunnel` when provisioning a specific resource. Without `resourceType`, `object-storage` specs create R2 buckets and `managed-db` specs create D1 databases.
1313

14+
Tunnel provisioning requires `tunnelSecret` in the Cloudflare cloud config. The adapter sends that caller-owned secret to Cloudflare and does not generate or return connector credentials.
15+
1416
## Package
1517

1618
- Name: `@profullstack/sh1pt-cloud-cloudflare`

packages/cloud/cloudflare/src/index.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ describe('Cloudflare cloud adapter', () => {
9292
it('lists supported Cloudflare resources', async () => {
9393
vi.stubGlobal('fetch', vi.fn(async (url: string) => {
9494
const { pathname } = new URL(url);
95-
if (pathname.endsWith('/r2/buckets')) return ok([{ name: 'assets', creation_date: '2026-06-14T00:00:00Z' }]);
95+
if (pathname.endsWith('/r2/buckets')) return ok({ buckets: [{ name: 'assets', creation_date: '2026-06-14T00:00:00Z' }] });
9696
if (pathname.endsWith('/d1/database')) return ok([{ uuid: 'db-1', name: 'main', created_at: '2026-06-14T00:00:00Z' }]);
9797
if (pathname.endsWith('/queues')) return ok([{ queue_id: 'queue-1', queue_name: 'jobs', created_on: '2026-06-14T00:00:00Z' }]);
9898
if (pathname.endsWith('/cfd_tunnel')) return ok([{ id: 'tun-1', name: 'edge', status: 'healthy', created_at: '2026-06-14T00:00:00Z' }]);
@@ -107,6 +107,8 @@ describe('Cloudflare cloud adapter', () => {
107107
'r2:assets',
108108
'tunnel:tun-1',
109109
]);
110+
expect(instances.find((instance) => instance.id === 'queue:queue-1')?.kind).toBe('object-storage');
111+
expect(instances.find((instance) => instance.id === 'tunnel:tun-1')?.kind).toBe('object-storage');
110112
expect(instances.find((instance) => instance.id === 'tunnel:tun-1')?.status).toBe('running');
111113
});
112114

@@ -122,6 +124,45 @@ describe('Cloudflare cloud adapter', () => {
122124
expect(instance).toMatchObject({ id: 'queue:queue-1', status: 'running', sku: 'queue' });
123125
});
124126

127+
it('requires a caller-supplied tunnel secret when creating a tunnel', async () => {
128+
const fetchMock = vi.fn();
129+
vi.stubGlobal('fetch', fetchMock);
130+
131+
await expect(adapter.provision(
132+
provisionCtx(),
133+
{ kind: 'object-storage', region: 'auto' },
134+
{ accountId: 'acct-1', resourceType: 'tunnel', name: 'edge' },
135+
)).rejects.toThrow('Cloudflare tunnel provisioning requires config.tunnelSecret');
136+
expect(fetchMock).not.toHaveBeenCalled();
137+
});
138+
139+
it('creates a tunnel with a caller-supplied tunnel secret', async () => {
140+
const fetchMock = vi.fn(async (url: string, init: RequestInit) => {
141+
expect(url).toBe(`${API}/accounts/acct-1/cfd_tunnel`);
142+
expect(init.method).toBe('POST');
143+
expect(JSON.parse(String(init.body))).toEqual({
144+
name: 'edge',
145+
config_src: 'cloudflare',
146+
tunnel_secret: 'known-secret',
147+
});
148+
return ok({ id: 'tun-1', name: 'edge', status: 'healthy', created_at: '2026-06-14T00:00:00Z' });
149+
});
150+
vi.stubGlobal('fetch', fetchMock);
151+
152+
const instance = await adapter.provision(
153+
provisionCtx(),
154+
{ kind: 'managed-db', region: 'auto' },
155+
{ accountId: 'acct-1', resourceType: 'tunnel', name: 'edge', tunnelSecret: 'known-secret' },
156+
);
157+
158+
expect(instance).toMatchObject({
159+
id: 'tunnel:tun-1',
160+
kind: 'object-storage',
161+
status: 'running',
162+
sku: 'tunnel',
163+
});
164+
});
165+
125166
it('deletes the prefixed resource id', async () => {
126167
const fetchMock = vi.fn(async (url: string, init: RequestInit) => {
127168
expect(url).toBe(`${API}/accounts/acct-1/cfd_tunnel/tun-1`);

packages/cloud/cloudflare/src/index.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { randomBytes } from 'node:crypto';
21
import {
32
defineCloud,
43
tokenSetup,
@@ -109,7 +108,8 @@ export default defineCloud<Config>({
109108
}
110109

111110
const name = safeName(config.name ?? `sh1pt-${resourceType}-${Date.now().toString(36)}`);
112-
if (ctx.dryRun) return dryRunInstance(resourceType, name, spec.kind, quote, spec.region);
111+
const kind = kindForResource(resourceType);
112+
if (ctx.dryRun) return dryRunInstance(resourceType, name, kind, quote, spec.region);
113113

114114
const accountId = await resolveAccountId(ctx, config);
115115

@@ -122,7 +122,7 @@ export default defineCloud<Config>({
122122
`/accounts/${encodeURIComponent(accountId)}/r2/buckets`,
123123
{ name },
124124
);
125-
return bucketInstance(result, spec.kind, quote, spec.region);
125+
return bucketInstance(result, kind, quote, spec.region);
126126
}
127127
case 'd1-database': {
128128
const { result } = await cfRequest<D1Database>(
@@ -135,7 +135,7 @@ export default defineCloud<Config>({
135135
primary_location_hint: spec.region ?? config.defaultRegion,
136136
},
137137
);
138-
return d1Instance(result, spec.kind, quote, spec.region ?? config.defaultRegion);
138+
return d1Instance(result, kind, quote, spec.region ?? config.defaultRegion);
139139
}
140140
case 'queue': {
141141
const { result } = await cfRequest<Queue>(
@@ -145,9 +145,12 @@ export default defineCloud<Config>({
145145
`/accounts/${encodeURIComponent(accountId)}/queues`,
146146
{ queue_name: name },
147147
);
148-
return queueInstance(result, spec.kind, quote, spec.region);
148+
return queueInstance(result, kind, quote, spec.region);
149149
}
150150
case 'tunnel': {
151+
if (!config.tunnelSecret) {
152+
throw new Error('Cloudflare tunnel provisioning requires config.tunnelSecret so the connector secret is not generated and lost');
153+
}
151154
const { result } = await cfRequest<Tunnel>(
152155
ctx,
153156
config,
@@ -156,10 +159,10 @@ export default defineCloud<Config>({
156159
{
157160
name,
158161
config_src: 'cloudflare',
159-
tunnel_secret: config.tunnelSecret ?? randomBytes(32).toString('base64'),
162+
tunnel_secret: config.tunnelSecret,
160163
},
161164
);
162-
return tunnelInstance(result, spec.kind, quote, spec.region);
165+
return tunnelInstance(result, kind, quote, spec.region);
163166
}
164167
}
165168
},
@@ -198,19 +201,19 @@ export default defineCloud<Config>({
198201
switch (resource.type) {
199202
case 'r2-bucket': {
200203
const { result } = await cfRequest<R2Bucket>(ctx, config, 'GET', `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(resource.nativeId)}`);
201-
return bucketInstance(result, 'object-storage', quote);
204+
return bucketInstance(result, kindForResource(resource.type), quote);
202205
}
203206
case 'd1-database': {
204207
const { result } = await cfRequest<D1Database>(ctx, config, 'GET', `/accounts/${encodeURIComponent(accountId)}/d1/database/${encodeURIComponent(resource.nativeId)}`);
205-
return d1Instance(result, 'managed-db', quote);
208+
return d1Instance(result, kindForResource(resource.type), quote);
206209
}
207210
case 'queue': {
208211
const { result } = await cfRequest<Queue>(ctx, config, 'GET', `/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(resource.nativeId)}`);
209-
return queueInstance(result, 'object-storage', quote);
212+
return queueInstance(result, kindForResource(resource.type), quote);
210213
}
211214
case 'tunnel': {
212215
const { result } = await cfRequest<Tunnel>(ctx, config, 'GET', `/accounts/${encodeURIComponent(accountId)}/cfd_tunnel/${encodeURIComponent(resource.nativeId)}`);
213-
return tunnelInstance(result, 'object-storage', quote);
216+
return tunnelInstance(result, kindForResource(resource.type), quote);
214217
}
215218
}
216219
},
@@ -240,16 +243,16 @@ async function listResource(
240243
switch (resourceType) {
241244
case 'r2-bucket':
242245
return (await cfListAll<R2Bucket>(ctx, config, `/accounts/${account}/r2/buckets`, 'buckets'))
243-
.map((bucket) => bucketInstance(bucket, 'object-storage', zeroQuote('r2-bucket')));
246+
.map((bucket) => bucketInstance(bucket, kindForResource('r2-bucket'), zeroQuote('r2-bucket')));
244247
case 'd1-database':
245248
return (await cfListAll<D1Database>(ctx, config, `/accounts/${account}/d1/database`, 'databases'))
246-
.map((db) => d1Instance(db, 'managed-db', zeroQuote('d1-database')));
249+
.map((db) => d1Instance(db, kindForResource('d1-database'), zeroQuote('d1-database')));
247250
case 'queue':
248251
return (await cfListAll<Queue>(ctx, config, `/accounts/${account}/queues`, 'queues'))
249-
.map((queue) => queueInstance(queue, 'object-storage', zeroQuote('queue')));
252+
.map((queue) => queueInstance(queue, kindForResource('queue'), zeroQuote('queue')));
250253
case 'tunnel':
251254
return (await cfListAll<Tunnel>(ctx, config, `/accounts/${account}/cfd_tunnel`, 'tunnels'))
252-
.map((tunnel) => tunnelInstance(tunnel, 'object-storage', zeroQuote('tunnel')));
255+
.map((tunnel) => tunnelInstance(tunnel, kindForResource('tunnel'), zeroQuote('tunnel')));
253256
}
254257
}
255258

@@ -345,6 +348,17 @@ function resourceTypeFor(spec: InstanceSpec, config: Config): ResourceType {
345348
throw new Error(`cloud-cloudflare supports object-storage and managed-db specs; got ${spec.kind}`);
346349
}
347350

351+
function kindForResource(resourceType: ResourceType): InstanceKind {
352+
switch (resourceType) {
353+
case 'r2-bucket':
354+
case 'queue':
355+
case 'tunnel':
356+
return 'object-storage';
357+
case 'd1-database':
358+
return 'managed-db';
359+
}
360+
}
361+
348362
function listResourceTypes(config: Config): ResourceType[] {
349363
if (config.resourceType === 'worker') {
350364
throw new Error('Cloudflare Workers scripts are handled by the deploy-workers target, not cloud-cloudflare');

0 commit comments

Comments
 (0)