Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/cloudflare-rate-limit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Resource } from "sst/resource";

export default {
async fetch(req: Request) {
const url = new URL(req.url);

const outcome = await Resource.MyRateLimit.limit({ key: url.pathname });
if (!outcome.success) {
return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 });
}

return new Response("OK", { status: 200 });
},
};
9 changes: 9 additions & 0 deletions examples/cloudflare-rate-limit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "cloudflare-rate-limit",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@cloudflare/workers-types": "^4.20260426.1",
"sst": "file:../../sdk/js"
}
}
33 changes: 33 additions & 0 deletions examples/cloudflare-rate-limit/sst.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/// <reference path="./.sst/platform/config.d.ts" />

/**
* ## Cloudflare Rate Limit
*
* This example creates a Cloudflare Rate Limit and a Worker that applies it.
*/
export default $config({
app(input) {
return {
name: "cloudflare-rate-limit",
removal: input?.stage === "production" ? "retain" : "remove",
home: "cloudflare",
};
},
async run() {
const rateLimit = new sst.cloudflare.RateLimit("MyRateLimit", {
namespaceId: 1001,
limit: 100,
period: "1 minute",
});

const worker = new sst.cloudflare.Worker("MyWorker", {
handler: "./index.ts",
url: true,
link: [rateLimit],
});

return {
api: worker.url,
};
},
});
6 changes: 6 additions & 0 deletions examples/cloudflare-rate-limit/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"lib": ["esnext"],
"types": ["@cloudflare/workers-types"]
}
}
1 change: 1 addition & 0 deletions pkg/types/typescript/typescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var mapping = map[string]string{
"hyperdriveBindings": "Hyperdrive",
"versionMetadataBindings": "WorkerVersionMetadata",
"workflowBindings": "Workflow",
"rateLimitBindings": "RateLimit",
}

var header = strings.Join([]string{
Expand Down
14 changes: 13 additions & 1 deletion platform/src/components/cloudflare/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ export interface WorkflowBinding {
};
}

export interface RateLimitBinding {
type: "rateLimitBindings";
properties: {
namespaceId: Input<string>;
simple: Input<{
limit: Input<number>;
period: Input<number>;
}>;
};
}

export type Binding =
| AiBinding
| KvBinding
Expand All @@ -99,7 +110,8 @@ export type Binding =
| D1DatabaseBinding
| HyperdriveBinding
| VersionMetadataBinding
| WorkflowBinding;
| WorkflowBinding
| RateLimitBinding;

export function binding<T extends Binding["type"]>(input: Binding & {}) {
return {
Expand Down
11 changes: 11 additions & 0 deletions platform/src/components/cloudflare/helpers/wrangler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function createWranglerConfig(input: {
const services: Record<string, any>[] = [];
const queueProducers: Record<string, any>[] = [];
const workflows: Record<string, any>[] = [];
const rateLimits: Record<string, any>[] = [];
let ai: Record<string, any> | undefined;
let versionMetadata: Record<string, any> | undefined;

Expand Down Expand Up @@ -135,6 +136,13 @@ export function createWranglerConfig(input: {
remote: true,
});
break;
case "rateLimitBindings":
rateLimits.push({
name: link.name,
namespace_id: stringValue(properties.namespaceId),
simple: properties.simple,
});
break;
}
}

Expand Down Expand Up @@ -170,6 +178,9 @@ export function createWranglerConfig(input: {
if (workflows.length > 0) {
config.workflows = workflows;
}
if (rateLimits.length > 0) {
config.rate_limits = rateLimits;
}

return config;
}
Expand Down
1 change: 1 addition & 0 deletions platform/src/components/cloudflare/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from "./astro";
export * from "./react-router";
export * from "./tan-stack-start";
export * from "./workflow";
export * from "./rate-limit";
export { binding } from "./binding.js";

/**
Expand Down
178 changes: 178 additions & 0 deletions platform/src/components/cloudflare/rate-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
ComponentResourceOptions,
output,
type Input,
type Output,
} from "@pulumi/pulumi";
import { Component } from "../component";
import { Link } from "../link";
import { binding } from "./binding";
import { toSeconds } from "../duration";
import { VisibleError } from "../error";

export interface RateLimitArgs {
/**
* A positive integer that uniquely defines this rate limiting namespace within your Cloudflare account.
*/
namespaceId: Input<number>;
/**
* The number of allowed requests within the specified period of time.
*/
limit: Input<number>;
/**
* The duration of the rate limit window. Must be either 10 seconds or 1 minute.
*/
period: Input<"10 seconds" | "1 minute">;
}

/**
* The `RateLimit` component lets you add a [Cloudflare Rate Limit](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) binding to
* your app.
*
* @example
*
* #### Minimal example
*
* ```ts title="sst.config.ts"
* const rateLimit = new sst.cloudflare.RateLimit("MyRateLimit", {
* namespaceId: 1001,
* limit: 100,
* period: "1 minute",
* });
* ```
*
* #### Link to a worker
*
* You can link RateLimit to a worker.
*
* ```ts {4} title="sst.config.ts"
* new sst.cloudflare.Worker("MyWorker", {
* handler: "./index.ts",
* url: true
* link: [rateLimit],
* });
* ```
*
* Once linked, you can use the SDK to interact with the RateLimit binding.
*
* ```ts title="index.ts" {7}
* import { Resource } from "sst/resource";
*
* export default {
* async fetch(req, env): Promise<Response> {
* const url = new URL(req.url);
*
* const outcome = await Resource.MyRateLimit.limit({ key: url.pathname });
* if (!outcome.success) {
* return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 });
* }
*
* return new Response("OK", { status: 200 });
* }
* }
* ```
*/
export class RateLimit extends Component implements Link.Linkable {
private _namespaceId: Output<string>;
private _limit: Output<number>;
private _period: Output<number>;

constructor(
name: string,
args: RateLimitArgs,
opts?: ComponentResourceOptions,
) {
super(__pulumiType, name, args, opts);

const namespaceId = normalizeNamespaceId();
const limit = output(args.limit);
const period = normalizePeriod();

this._namespaceId = namespaceId;
this._limit = limit;
this._period = period;

function normalizeNamespaceId() {
return output(args.namespaceId).apply((namespaceId) => {
if (!Number.isInteger(namespaceId) || namespaceId <= 0) {
throw new VisibleError(
"The `namespaceId` property must be a positive integer.",
);
}

return namespaceId.toString();
});
}

function normalizePeriod() {
return output(args.period).apply(toSeconds);
}
}

/**
* A unique identifier for the rate limit namespace.
*/
public get namespaceId() {
return this._namespaceId;
}

/**
* The number of allowed requests within the specified period of time.
*/
public get limit() {
return this._limit;
}

/**
* The duration of the rate limit window, in seconds.
*/
public get period() {
return this._period;
}

/**
* When you link a RateLimit binding, it will be available to the worker and you can
* interact with it using its [API methods](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/).
*
* @example
* ```ts title="index.ts"
* import { Resource } from "sst/resource";
*
* export default {
* async fetch(req, env): Promise<Response> {
* const url = new URL(req.url);
*
* const outcome = await Resource.MyRateLimit.limit({ key: url.pathname });
* if (!outcome.success) {
* return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 });
* }
*
* return new Response("OK", { status: 200 });
* }
* }
* ```
*
* @internal
*/
getSSTLink() {
return {
properties: {},
include: [
binding({
type: "rateLimitBindings",
properties: {
namespaceId: this._namespaceId,
simple: {
limit: this._limit,
period: this._period,
},
},
}),
],
};
}
}

const __pulumiType = "sst:cloudflare:RateLimit";
// @ts-expect-error
RateLimit.__pulumiType = __pulumiType;
1 change: 1 addition & 0 deletions platform/src/components/cloudflare/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ export class Worker extends Component implements Link.Linkable {
hyperdriveBindings: "hyperdrive",
versionMetadataBindings: "version_metadata",
workflowBindings: "workflow",
rateLimitBindings: "ratelimit"
}[b.binding],
name,
...b.properties,
Expand Down
1 change: 1 addition & 0 deletions www/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const sidebar = [
"docs/component/cloudflare/worker",
"docs/component/cloudflare/bucket",
"docs/component/cloudflare/workflow",
"docs/component/cloudflare/rate-limit",
{ label: "StaticSite", slug: "docs/component/cloudflare/static-site-v2" },
"docs/component/cloudflare/hyperdrive",
"docs/component/cloudflare/react-router",
Expand Down
1 change: 1 addition & 0 deletions www/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2537,6 +2537,7 @@ async function buildComponents() {
"../platform/src/components/cloudflare/react-router.ts",
"../platform/src/components/cloudflare/worker.ts",
"../platform/src/components/cloudflare/workflow.ts",
"../platform/src/components/cloudflare/rate-limit.ts",
"../platform/src/components/cloudflare/static-site.ts",
"../platform/src/components/cloudflare/static-site-v2.ts",
"../platform/src/components/cloudflare/tan-stack-start.ts",
Expand Down
2 changes: 1 addition & 1 deletion www/src/content/docs/docs/cloudflare.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ For scheduled work, use [`Cron`](/docs/component/cloudflare/cron/) to run a work

### More components

Browse the component docs for [`Worker`](/docs/component/cloudflare/worker/), [`Astro`](/docs/component/cloudflare/astro/), [`Bucket`](/docs/component/cloudflare/bucket/), [`D1`](/docs/component/cloudflare/d1/), [`Kv`](/docs/component/cloudflare/kv/), [`Queue`](/docs/component/cloudflare/queue/), [`Cron`](/docs/component/cloudflare/cron/), [`Ai`](/docs/component/cloudflare/ai/), and [`Workflow`](/docs/component/cloudflare/workflow/).
Browse the component docs for [`Worker`](/docs/component/cloudflare/worker/), [`Astro`](/docs/component/cloudflare/astro/), [`Bucket`](/docs/component/cloudflare/bucket/), [`D1`](/docs/component/cloudflare/d1/), [`Kv`](/docs/component/cloudflare/kv/), [`Queue`](/docs/component/cloudflare/queue/), [`Cron`](/docs/component/cloudflare/cron/), [`Ai`](/docs/component/cloudflare/ai/), [`Workflow`](/docs/component/cloudflare/workflow/), and [`RateLimit`](/docs/component/cloudflare/rate-limit/).

If you are using Cloudflare DNS with SST, use [`sst.cloudflare.dns`](/docs/component/cloudflare/dns/) with [custom domains](/docs/custom-domains/).

Expand Down
Loading