diff --git a/package-lock.json b/package-lock.json
index 18fef4f..1866898 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
"@types/node": "^25.5.0",
"run-p": "^0.0.0",
"typescript": "^5.9.3",
- "wrangler": "^4.73.0"
+ "wrangler": "^4.77.0"
}
},
"node_modules/@atcute/cbor": {
@@ -48,9 +48,9 @@
"license": "MIT"
},
"node_modules/@atcute/multibase": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/@atcute/multibase/-/multibase-1.1.8.tgz",
- "integrity": "sha512-pJgtImMZKCjqwRbu+2GzB+4xQjKBXDwdZOzeqe0u97zYKRGftpGYGvYv3+pMe2xXe+msDyu7Nv8iJp+U14otTA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@atcute/multibase/-/multibase-1.2.0.tgz",
+ "integrity": "sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==",
"license": "0BSD",
"dependencies": {
"@atcute/uint8array": "^1.1.1"
@@ -79,9 +79,9 @@
}
},
"node_modules/@cloudflare/unenv-preset": {
- "version": "2.15.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.15.0.tgz",
- "integrity": "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==",
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.0.tgz",
+ "integrity": "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"peerDependencies": {
@@ -95,9 +95,9 @@
}
},
"node_modules/@cloudflare/workerd-darwin-64": {
- "version": "1.20260312.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260312.1.tgz",
- "integrity": "sha512-HUAtDWaqUduS6yasV6+NgsK7qBpP1qGU49ow/Wb117IHjYp+PZPUGReDYocpB4GOMRoQlvdd4L487iFxzdARpw==",
+ "version": "1.20260317.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260317.1.tgz",
+ "integrity": "sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==",
"cpu": [
"x64"
],
@@ -112,9 +112,9 @@
}
},
"node_modules/@cloudflare/workerd-darwin-arm64": {
- "version": "1.20260312.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260312.1.tgz",
- "integrity": "sha512-DOn7TPTHSxJYfi4m4NYga/j32wOTqvJf/pY4Txz5SDKWIZHSTXFyGz2K4B+thoPWLop/KZxGoyTv7db0mk/qyw==",
+ "version": "1.20260317.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260317.1.tgz",
+ "integrity": "sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==",
"cpu": [
"arm64"
],
@@ -129,9 +129,9 @@
}
},
"node_modules/@cloudflare/workerd-linux-64": {
- "version": "1.20260312.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260312.1.tgz",
- "integrity": "sha512-TdkIh3WzPXYHuvz7phAtFEEvAxvFd30tHrm4gsgpw0R0F5b8PtoM3hfL2uY7EcBBWVYUBtkY2ahDYFfufnXw/g==",
+ "version": "1.20260317.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260317.1.tgz",
+ "integrity": "sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==",
"cpu": [
"x64"
],
@@ -146,9 +146,9 @@
}
},
"node_modules/@cloudflare/workerd-linux-arm64": {
- "version": "1.20260312.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260312.1.tgz",
- "integrity": "sha512-kNauZhL569Iy94t844OMwa1zP6zKFiL3xiJ4tGLS+TFTEfZ3pZsRH6lWWOtkXkjTyCmBEOog0HSEKjIV4oAffw==",
+ "version": "1.20260317.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260317.1.tgz",
+ "integrity": "sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==",
"cpu": [
"arm64"
],
@@ -163,9 +163,9 @@
}
},
"node_modules/@cloudflare/workerd-windows-64": {
- "version": "1.20260312.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260312.1.tgz",
- "integrity": "sha512-5dBrlSK+nMsZy5bYQpj8t9iiQNvCRlkm9GGvswJa9vVU/1BNO4BhJMlqOLWT24EmFyApZ+kaBiPJMV8847NDTg==",
+ "version": "1.20260317.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260317.1.tgz",
+ "integrity": "sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==",
"cpu": [
"x64"
],
@@ -193,9 +193,9 @@
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
- "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1173,16 +1173,6 @@
"kleur": "^4.1.5"
}
},
- "node_modules/@poppinss/colors/node_modules/kleur": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
- "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/@poppinss/dumper": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
@@ -1216,9 +1206,9 @@
}
},
"node_modules/@speed-highlight/core": {
- "version": "1.2.14",
- "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz",
- "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==",
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz",
+ "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==",
"dev": true,
"license": "CC0-1.0"
},
@@ -1239,6 +1229,30 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/error-stack-parser-es": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
@@ -1318,17 +1332,27 @@
"integrity": "sha512-tYzMAz72bJXRBFKhWnEZrVdBbBqV4/vwNGv6O96LqZv+BjtmUGUBtBkFr+vqGlgHOVIcRalUNdJlumvXSAbuaQ==",
"license": "MIT"
},
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/miniflare": {
- "version": "4.20260312.0",
- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260312.0.tgz",
- "integrity": "sha512-pieP2rfXynPT6VRINYaiHe/tfMJ4c5OIhqRlIdLF6iZ9g5xgpEmvimvIgMpgAdDJuFlrLcwDUi8MfAo2R6dt/w==",
+ "version": "4.20260317.2",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260317.2.tgz",
+ "integrity": "sha512-qNL+yWAFMX6fr0pWU6Lx1vNpPobpnDSF1V8eunIckWvoIQl8y1oBjL2RJFEGY3un+l3f9gwW9dirDPP26usYJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "0.8.1",
"sharp": "^0.34.5",
- "undici": "7.18.2",
- "workerd": "1.20260312.1",
+ "undici": "7.24.4",
+ "workerd": "1.20260317.1",
"ws": "8.18.0",
"youch": "4.1.0-beta.10"
},
@@ -1339,28 +1363,6 @@
"node": ">=18.0.0"
}
},
- "node_modules/miniflare/node_modules/ws": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
- "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
"node_modules/multiformats": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz",
@@ -1446,16 +1448,6 @@
"@img/sharp-win32-x64": "0.34.5"
}
},
- "node_modules/sharp/node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
@@ -1501,9 +1493,9 @@
}
},
"node_modules/undici": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.0.tgz",
- "integrity": "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==",
+ "version": "7.24.4",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
+ "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1528,9 +1520,9 @@
}
},
"node_modules/workerd": {
- "version": "1.20260312.1",
- "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260312.1.tgz",
- "integrity": "sha512-nNpPkw9jaqo79B+iBCOiksx+N62xC+ETIfyzofUEdY3cSOHJg6oNnVSHm7vHevzVblfV76c8Gr0cXHEapYMBEg==",
+ "version": "1.20260317.1",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260317.1.tgz",
+ "integrity": "sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -1541,41 +1533,41 @@
"node": ">=16"
},
"optionalDependencies": {
- "@cloudflare/workerd-darwin-64": "1.20260312.1",
- "@cloudflare/workerd-darwin-arm64": "1.20260312.1",
- "@cloudflare/workerd-linux-64": "1.20260312.1",
- "@cloudflare/workerd-linux-arm64": "1.20260312.1",
- "@cloudflare/workerd-windows-64": "1.20260312.1"
+ "@cloudflare/workerd-darwin-64": "1.20260317.1",
+ "@cloudflare/workerd-darwin-arm64": "1.20260317.1",
+ "@cloudflare/workerd-linux-64": "1.20260317.1",
+ "@cloudflare/workerd-linux-arm64": "1.20260317.1",
+ "@cloudflare/workerd-windows-64": "1.20260317.1"
}
},
"node_modules/wrangler": {
- "version": "4.73.0",
- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.73.0.tgz",
- "integrity": "sha512-VJXsqKDFCp6OtFEHXITSOR5kh95JOknwPY8m7RyQuWJQguSybJy43m4vhoCSt42prutTef7eeuw7L4V4xiynGw==",
+ "version": "4.77.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.77.0.tgz",
+ "integrity": "sha512-E2Gm69+K++BFd3QvoWjC290RPQj1vDOUotA++sNHmtKPb7EP6C8Qv+1D5Ii73tfZtyNgakpqHlh8lBBbVWTKAQ==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
"@cloudflare/kv-asset-handler": "0.4.2",
- "@cloudflare/unenv-preset": "2.15.0",
+ "@cloudflare/unenv-preset": "2.16.0",
"blake3-wasm": "2.1.5",
"esbuild": "0.27.3",
- "miniflare": "4.20260312.0",
+ "miniflare": "4.20260317.2",
"path-to-regexp": "6.3.0",
"unenv": "2.0.0-rc.24",
- "workerd": "1.20260312.1"
+ "workerd": "1.20260317.1"
},
"bin": {
"wrangler": "bin/wrangler.js",
"wrangler2": "bin/wrangler.js"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=20.3.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
- "@cloudflare/workers-types": "^4.20260312.1"
+ "@cloudflare/workers-types": "^4.20260317.1"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
@@ -1583,6 +1575,28 @@
}
}
},
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/youch": {
"version": "4.1.0-beta.10",
"resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
@@ -1607,20 +1621,6 @@
"@poppinss/exception": "^1.2.2",
"error-stack-parser-es": "^1.0.5"
}
- },
- "node_modules/youch/node_modules/cookie": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
- "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
}
}
}
diff --git a/package.json b/package.json
index a89541f..0d6ac97 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "simplebskylabeler",
- "version": "1.0.0",
+ "version": "1.2.0",
"private": true,
"scripts": {
"createdb": "wrangler d1 create bskylabels --binding DB --update-config",
@@ -16,7 +16,7 @@
"@types/node": "^25.5.0",
"run-p": "^0.0.0",
"typescript": "^5.9.3",
- "wrangler": "^4.73.0"
+ "wrangler": "^4.77.0"
},
"dependencies": {
"@atcute/cbor": "^1.0.7",
@@ -24,10 +24,5 @@
"just-has": "^2.3.0",
"just-template": "^2.2.0",
"uint8arrays": "^5.1.0"
- },
- "overrides": {
- "miniflare": {
- "undici": "^7.24.0"
- }
}
}
diff --git a/src/index.ts b/src/index.ts
index 5adfd11..acbefcc 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -128,7 +128,14 @@ export default {
}
}
return response;
- } else if (url.pathname === "/add-account" || url.pathname === "/add-self") {
+ } else if (url.pathname === "/add-account") {
+ if (env.RATELIMIT === "true") {
+ const ipAddress = request.headers.get("cf-connecting-ip") || "";
+ const { success } = await env.BSKY_LABEL_LIMITER.limit({ key: ipAddress });
+ if (!success) {
+ return new Response("ERROR: You are adding too quickly, please slow down", {status: 429});
+ }
+ }
// drop non post requests
if (request.method !== "POST") {
return new Response("ERROR: You cannot submit data this way", {status: 405});
diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts
index a8263a6..4a1b19c 100644
--- a/worker-configuration.d.ts
+++ b/worker-configuration.d.ts
@@ -1,14 +1,16 @@
/* eslint-disable */
-// Generated by Wrangler by running `wrangler types` (hash: a3a025b6baf0beefc4d917e42f0f0b05)
-// Runtime types generated with workerd@1.20260312.1 2024-12-18 nodejs_compat
+// Generated by Wrangler by running `wrangler types` (hash: 4fbff60ff6193980e25186a76790a338)
+// Runtime types generated with workerd@1.20260317.1 2024-12-18 nodejs_compat
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/index");
}
interface Env {
DB: D1Database;
+ BSKY_LABEL_LIMITER: RateLimit;
ASSETS: Fetcher;
COUNT_CACHE_HOUR: "4";
+ RATELIMIT: "true";
SITE_TITLE: "Simple BSky Labeler";
SITE_DESCRIPTION: "Allow people to obtain a bsky label you provide";
SITE_SHORTNAME: "SBL";
@@ -3293,6 +3295,7 @@ interface WorkerStubEntrypointOptions {
}
interface WorkerLoader {
get(name: string | null, getCode: () => WorkerLoaderWorkerCode | Promise): WorkerStub;
+ load(code: WorkerLoaderWorkerCode): WorkerStub;
}
interface WorkerLoaderModule {
js?: string;
@@ -10687,6 +10690,752 @@ declare module "cloudflare:sockets" {
function _connect(address: string | SocketAddress, options?: SocketOptions): Socket;
export { _connect as connect };
}
+/**
+ * Binding entrypoint for Cloudflare Stream.
+ *
+ * Usage:
+ * - Binding-level operations:
+ * `await env.STREAM.videos.upload`
+ * `await env.STREAM.videos.createDirectUpload`
+ * `await env.STREAM.videos.*`
+ * `await env.STREAM.watermarks.*`
+ * - Per-video operations:
+ * `await env.STREAM.video(id).downloads.*`
+ * `await env.STREAM.video(id).captions.*`
+ *
+ * Example usage:
+ * ```ts
+ * await env.STREAM.video(id).downloads.generate();
+ *
+ * const video = env.STREAM.video(id)
+ * const captions = video.captions.list();
+ * const videoDetails = video.details()
+ * ```
+ */
+interface StreamBinding {
+ /**
+ * Returns a handle scoped to a single video for per-video operations.
+ * @param id The unique identifier for the video.
+ * @returns A handle for per-video operations.
+ */
+ video(id: string): StreamVideoHandle;
+ /**
+ * Uploads a new video from a File.
+ * @param file The video file to upload.
+ * @returns The uploaded video details.
+ * @throws {BadRequestError} if the upload parameter is invalid
+ * @throws {QuotaReachedError} if the account storage capacity is exceeded
+ * @throws {MaxFileSizeError} if the file size is too large
+ * @throws {RateLimitedError} if the server received too many requests
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ upload(file: File): Promise;
+ /**
+ * Uploads a new video from a provided URL.
+ * @param url The URL to upload from.
+ * @param params Optional upload parameters.
+ * @returns The uploaded video details.
+ * @throws {BadRequestError} if the upload parameter is invalid or the URL is invalid
+ * @throws {QuotaReachedError} if the account storage capacity is exceeded
+ * @throws {MaxFileSizeError} if the file size is too large
+ * @throws {RateLimitedError} if the server received too many requests
+ * @throws {AlreadyUploadedError} if a video was already uploaded to this URL
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ upload(url: string, params?: StreamUrlUploadParams): Promise;
+ /**
+ * Creates a direct upload that allows video uploads without an API key.
+ * @param params Parameters for the direct upload
+ * @returns The direct upload details.
+ * @throws {BadRequestError} if the parameters are invalid
+ * @throws {RateLimitedError} if the server received too many requests
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ createDirectUpload(params: StreamDirectUploadCreateParams): Promise;
+ videos: StreamVideos;
+ watermarks: StreamWatermarks;
+}
+/**
+ * Handle for operations scoped to a single Stream video.
+ */
+interface StreamVideoHandle {
+ /**
+ * The unique identifier for the video.
+ */
+ id: string;
+ /**
+ * Get a full videos details
+ * @returns The full video details.
+ * @throws {NotFoundError} if the video is not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ details(): Promise;
+ /**
+ * Update details for a single video.
+ * @param params The fields to update for the video.
+ * @returns The updated video details.
+ * @throws {NotFoundError} if the video is not found
+ * @throws {BadRequestError} if the parameters are invalid
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ update(params: StreamUpdateVideoParams): Promise;
+ /**
+ * Deletes a video and its copies from Cloudflare Stream.
+ * @returns A promise that resolves when deletion completes.
+ * @throws {NotFoundError} if the video is not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ delete(): Promise;
+ /**
+ * Creates a signed URL token for a video.
+ * @returns The signed token that was created.
+ * @throws {InternalError} if the signing key cannot be retrieved or the token cannot be signed
+ */
+ generateToken(): Promise;
+ downloads: StreamScopedDownloads;
+ captions: StreamScopedCaptions;
+}
+interface StreamVideo {
+ /**
+ * The unique identifier for the video.
+ */
+ id: string;
+ /**
+ * A user-defined identifier for the media creator.
+ */
+ creator: string | null;
+ /**
+ * The thumbnail URL for the video.
+ */
+ thumbnail: string;
+ /**
+ * The thumbnail timestamp percentage.
+ */
+ thumbnailTimestampPct: number;
+ /**
+ * Indicates whether the video is ready to stream.
+ */
+ readyToStream: boolean;
+ /**
+ * The date and time the video became ready to stream.
+ */
+ readyToStreamAt: string | null;
+ /**
+ * Processing status information.
+ */
+ status: StreamVideoStatus;
+ /**
+ * A user modifiable key-value store.
+ */
+ meta: Record;
+ /**
+ * The date and time the video was created.
+ */
+ created: string;
+ /**
+ * The date and time the video was last modified.
+ */
+ modified: string;
+ /**
+ * The date and time at which the video will be deleted.
+ */
+ scheduledDeletion: string | null;
+ /**
+ * The size of the video in bytes.
+ */
+ size: number;
+ /**
+ * The preview URL for the video.
+ */
+ preview?: string;
+ /**
+ * Origins allowed to display the video.
+ */
+ allowedOrigins: Array;
+ /**
+ * Indicates whether signed URLs are required.
+ */
+ requireSignedURLs: boolean | null;
+ /**
+ * The date and time the video was uploaded.
+ */
+ uploaded: string | null;
+ /**
+ * The date and time when the upload URL expires.
+ */
+ uploadExpiry: string | null;
+ /**
+ * The maximum size in bytes for direct uploads.
+ */
+ maxSizeBytes: number | null;
+ /**
+ * The maximum duration in seconds for direct uploads.
+ */
+ maxDurationSeconds: number | null;
+ /**
+ * The video duration in seconds. -1 indicates unknown.
+ */
+ duration: number;
+ /**
+ * Input metadata for the original upload.
+ */
+ input: StreamVideoInput;
+ /**
+ * Playback URLs for the video.
+ */
+ hlsPlaybackUrl: string;
+ dashPlaybackUrl: string;
+ /**
+ * The watermark applied to the video, if any.
+ */
+ watermark: StreamWatermark | null;
+ /**
+ * The live input id associated with the video, if any.
+ */
+ liveInputId?: string | null;
+ /**
+ * The source video id if this is a clip.
+ */
+ clippedFromId: string | null;
+ /**
+ * Public details associated with the video.
+ */
+ publicDetails: StreamPublicDetails | null;
+}
+type StreamVideoStatus = {
+ /**
+ * The current processing state.
+ */
+ state: string;
+ /**
+ * The current processing step.
+ */
+ step?: string;
+ /**
+ * The percent complete as a string.
+ */
+ pctComplete?: string;
+ /**
+ * An error reason code, if applicable.
+ */
+ errorReasonCode: string;
+ /**
+ * An error reason text, if applicable.
+ */
+ errorReasonText: string;
+};
+type StreamVideoInput = {
+ /**
+ * The input width in pixels.
+ */
+ width: number;
+ /**
+ * The input height in pixels.
+ */
+ height: number;
+};
+type StreamPublicDetails = {
+ /**
+ * The public title for the video.
+ */
+ title: string | null;
+ /**
+ * The public share link.
+ */
+ share_link: string | null;
+ /**
+ * The public channel link.
+ */
+ channel_link: string | null;
+ /**
+ * The public logo URL.
+ */
+ logo: string | null;
+};
+type StreamDirectUpload = {
+ /**
+ * The URL an unauthenticated upload can use for a single multipart request.
+ */
+ uploadURL: string;
+ /**
+ * A Cloudflare-generated unique identifier for a media item.
+ */
+ id: string;
+ /**
+ * The watermark profile applied to the upload.
+ */
+ watermark: StreamWatermark | null;
+ /**
+ * The scheduled deletion time, if any.
+ */
+ scheduledDeletion: string | null;
+};
+type StreamDirectUploadCreateParams = {
+ /**
+ * The maximum duration in seconds for a video upload.
+ */
+ maxDurationSeconds: number;
+ /**
+ * The date and time after upload when videos will not be accepted.
+ */
+ expiry?: string;
+ /**
+ * A user-defined identifier for the media creator.
+ */
+ creator?: string;
+ /**
+ * A user modifiable key-value store used to reference other systems of record for
+ * managing videos.
+ */
+ meta?: Record;
+ /**
+ * Lists the origins allowed to display the video.
+ */
+ allowedOrigins?: Array;
+ /**
+ * Indicates whether the video can be accessed using the id. When set to `true`,
+ * a signed token must be generated with a signing key to view the video.
+ */
+ requireSignedURLs?: boolean;
+ /**
+ * The thumbnail timestamp percentage.
+ */
+ thumbnailTimestampPct?: number;
+ /**
+ * The date and time at which the video will be deleted. Include `null` to remove
+ * a scheduled deletion.
+ */
+ scheduledDeletion?: string | null;
+ /**
+ * The watermark profile to apply.
+ */
+ watermark?: StreamDirectUploadWatermark;
+};
+type StreamDirectUploadWatermark = {
+ /**
+ * The unique identifier for the watermark profile.
+ */
+ id: string;
+};
+type StreamUrlUploadParams = {
+ /**
+ * Lists the origins allowed to display the video. Enter allowed origin
+ * domains in an array and use `*` for wildcard subdomains. Empty arrays allow the
+ * video to be viewed on any origin.
+ */
+ allowedOrigins?: Array;
+ /**
+ * A user-defined identifier for the media creator.
+ */
+ creator?: string;
+ /**
+ * A user modifiable key-value store used to reference other systems of
+ * record for managing videos.
+ */
+ meta?: Record;
+ /**
+ * Indicates whether the video can be a accessed using the id. When
+ * set to `true`, a signed token must be generated with a signing key to view the
+ * video.
+ */
+ requireSignedURLs?: boolean;
+ /**
+ * Indicates the date and time at which the video will be deleted. Omit
+ * the field to indicate no change, or include with a `null` value to remove an
+ * existing scheduled deletion. If specified, must be at least 30 days from upload
+ * time.
+ */
+ scheduledDeletion?: string | null;
+ /**
+ * The timestamp for a thumbnail image calculated as a percentage value
+ * of the video's duration. To convert from a second-wise timestamp to a
+ * percentage, divide the desired timestamp by the total duration of the video. If
+ * this value is not set, the default thumbnail image is taken from 0s of the
+ * video.
+ */
+ thumbnailTimestampPct?: number;
+ /**
+ * The identifier for the watermark profile
+ */
+ watermarkId?: string;
+};
+interface StreamScopedCaptions {
+ /**
+ * Uploads the caption or subtitle file to the endpoint for a specific BCP47 language.
+ * One caption or subtitle file per language is allowed.
+ * @param language The BCP 47 language tag for the caption or subtitle.
+ * @param file The caption or subtitle file to upload.
+ * @returns The created caption entry.
+ * @throws {NotFoundError} if the video is not found
+ * @throws {BadRequestError} if the language or file is invalid
+ * @throws {MaxFileSizeError} if the file size is too large
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ upload(language: string, file: File): Promise;
+ /**
+ * Generate captions or subtitles for the provided language via AI.
+ * @param language The BCP 47 language tag to generate.
+ * @returns The generated caption entry.
+ * @throws {NotFoundError} if the video is not found
+ * @throws {BadRequestError} if the language is invalid
+ * @throws {StreamError} if a generated caption already exists
+ * @throws {StreamError} if the video duration is too long
+ * @throws {StreamError} if the video is missing audio
+ * @throws {StreamError} if the requested language is not supported
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ generate(language: string): Promise;
+ /**
+ * Lists the captions or subtitles.
+ * Use the language parameter to filter by a specific language.
+ * @param language The optional BCP 47 language tag to filter by.
+ * @returns The list of captions or subtitles.
+ * @throws {NotFoundError} if the video or caption is not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ list(language?: string): Promise;
+ /**
+ * Removes the captions or subtitles from a video.
+ * @param language The BCP 47 language tag to remove.
+ * @returns A promise that resolves when deletion completes.
+ * @throws {NotFoundError} if the video or caption is not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ delete(language: string): Promise;
+}
+interface StreamScopedDownloads {
+ /**
+ * Generates a download for a video when a video is ready to view. Available
+ * types are `default` and `audio`. Defaults to `default` when omitted.
+ * @param downloadType The download type to create.
+ * @returns The current downloads for the video.
+ * @throws {NotFoundError} if the video is not found
+ * @throws {BadRequestError} if the download type is invalid
+ * @throws {StreamError} if the video duration is too long to generate a download
+ * @throws {StreamError} if the video is not ready to stream
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ generate(downloadType?: StreamDownloadType): Promise;
+ /**
+ * Lists the downloads created for a video.
+ * @returns The current downloads for the video.
+ * @throws {NotFoundError} if the video or downloads are not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ get(): Promise;
+ /**
+ * Delete the downloads for a video. Available types are `default` and `audio`.
+ * Defaults to `default` when omitted.
+ * @param downloadType The download type to delete.
+ * @returns A promise that resolves when deletion completes.
+ * @throws {NotFoundError} if the video or downloads are not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ delete(downloadType?: StreamDownloadType): Promise;
+}
+interface StreamVideos {
+ /**
+ * Lists all videos in a users account.
+ * @returns The list of videos.
+ * @throws {BadRequestError} if the parameters are invalid
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ list(params?: StreamVideosListParams): Promise;
+}
+interface StreamWatermarks {
+ /**
+ * Generate a new watermark profile
+ * @param file The image file to upload
+ * @param params The watermark creation parameters.
+ * @returns The created watermark profile.
+ * @throws {BadRequestError} if the parameters are invalid
+ * @throws {InvalidURLError} if the URL is invalid
+ * @throws {MaxFileSizeError} if the file size is too large
+ * @throws {TooManyWatermarksError} if the number of allowed watermarks is reached
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ generate(file: File, params: StreamWatermarkCreateParams): Promise;
+ /**
+ * Generate a new watermark profile
+ * @param url The image url to upload
+ * @param params The watermark creation parameters.
+ * @returns The created watermark profile.
+ * @throws {BadRequestError} if the parameters are invalid
+ * @throws {InvalidURLError} if the URL is invalid
+ * @throws {MaxFileSizeError} if the file size is too large
+ * @throws {TooManyWatermarksError} if the number of allowed watermarks is reached
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ generate(url: string, params: StreamWatermarkCreateParams): Promise;
+ /**
+ * Lists all watermark profiles for an account.
+ * @returns The list of watermark profiles.
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ list(): Promise;
+ /**
+ * Retrieves details for a single watermark profile.
+ * @param watermarkId The watermark profile identifier.
+ * @returns The watermark profile details.
+ * @throws {NotFoundError} if the watermark is not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ get(watermarkId: string): Promise;
+ /**
+ * Deletes a watermark profile.
+ * @param watermarkId The watermark profile identifier.
+ * @returns A promise that resolves when deletion completes.
+ * @throws {NotFoundError} if the watermark is not found
+ * @throws {InternalError} if an unexpected error occurs
+ */
+ delete(watermarkId: string): Promise;
+}
+type StreamUpdateVideoParams = {
+ /**
+ * Lists the origins allowed to display the video. Enter allowed origin
+ * domains in an array and use `*` for wildcard subdomains. Empty arrays allow the
+ * video to be viewed on any origin.
+ */
+ allowedOrigins?: Array;
+ /**
+ * A user-defined identifier for the media creator.
+ */
+ creator?: string;
+ /**
+ * The maximum duration in seconds for a video upload. Can be set for a
+ * video that is not yet uploaded to limit its duration. Uploads that exceed the
+ * specified duration will fail during processing. A value of `-1` means the value
+ * is unknown.
+ */
+ maxDurationSeconds?: number;
+ /**
+ * A user modifiable key-value store used to reference other systems of
+ * record for managing videos.
+ */
+ meta?: Record;
+ /**
+ * Indicates whether the video can be a accessed using the id. When
+ * set to `true`, a signed token must be generated with a signing key to view the
+ * video.
+ */
+ requireSignedURLs?: boolean;
+ /**
+ * Indicates the date and time at which the video will be deleted. Omit
+ * the field to indicate no change, or include with a `null` value to remove an
+ * existing scheduled deletion. If specified, must be at least 30 days from upload
+ * time.
+ */
+ scheduledDeletion?: string | null;
+ /**
+ * The timestamp for a thumbnail image calculated as a percentage value
+ * of the video's duration. To convert from a second-wise timestamp to a
+ * percentage, divide the desired timestamp by the total duration of the video. If
+ * this value is not set, the default thumbnail image is taken from 0s of the
+ * video.
+ */
+ thumbnailTimestampPct?: number;
+};
+type StreamCaption = {
+ /**
+ * Whether the caption was generated via AI.
+ */
+ generated?: boolean;
+ /**
+ * The language label displayed in the native language to users.
+ */
+ label: string;
+ /**
+ * The language tag in BCP 47 format.
+ */
+ language: string;
+ /**
+ * The status of a generated caption.
+ */
+ status?: 'ready' | 'inprogress' | 'error';
+};
+type StreamDownloadStatus = 'ready' | 'inprogress' | 'error';
+type StreamDownloadType = 'default' | 'audio';
+type StreamDownload = {
+ /**
+ * Indicates the progress as a percentage between 0 and 100.
+ */
+ percentComplete: number;
+ /**
+ * The status of a generated download.
+ */
+ status: StreamDownloadStatus;
+ /**
+ * The URL to access the generated download.
+ */
+ url?: string;
+};
+/**
+ * An object with download type keys. Each key is optional and only present if that
+ * download type has been created.
+ */
+type StreamDownloadGetResponse = {
+ /**
+ * The audio-only download. Only present if this download type has been created.
+ */
+ audio?: StreamDownload;
+ /**
+ * The default video download. Only present if this download type has been created.
+ */
+ default?: StreamDownload;
+};
+type StreamWatermarkPosition = 'upperRight' | 'upperLeft' | 'lowerLeft' | 'lowerRight' | 'center';
+type StreamWatermark = {
+ /**
+ * The unique identifier for a watermark profile.
+ */
+ id: string;
+ /**
+ * The size of the image in bytes.
+ */
+ size: number;
+ /**
+ * The height of the image in pixels.
+ */
+ height: number;
+ /**
+ * The width of the image in pixels.
+ */
+ width: number;
+ /**
+ * The date and a time a watermark profile was created.
+ */
+ created: string;
+ /**
+ * The source URL for a downloaded image. If the watermark profile was created via
+ * direct upload, this field is null.
+ */
+ downloadedFrom: string | null;
+ /**
+ * A short description of the watermark profile.
+ */
+ name: string;
+ /**
+ * The translucency of the image. A value of `0.0` makes the image completely
+ * transparent, and `1.0` makes the image completely opaque. Note that if the image
+ * is already semi-transparent, setting this to `1.0` will not make the image
+ * completely opaque.
+ */
+ opacity: number;
+ /**
+ * The whitespace between the adjacent edges (determined by position) of the video
+ * and the image. `0.0` indicates no padding, and `1.0` indicates a fully padded
+ * video width or length, as determined by the algorithm.
+ */
+ padding: number;
+ /**
+ * The size of the image relative to the overall size of the video. This parameter
+ * will adapt to horizontal and vertical videos automatically. `0.0` indicates no
+ * scaling (use the size of the image as-is), and `1.0 `fills the entire video.
+ */
+ scale: number;
+ /**
+ * The location of the image. Valid positions are: `upperRight`, `upperLeft`,
+ * `lowerLeft`, `lowerRight`, and `center`. Note that `center` ignores the
+ * `padding` parameter.
+ */
+ position: StreamWatermarkPosition;
+};
+type StreamWatermarkCreateParams = {
+ /**
+ * A short description of the watermark profile.
+ */
+ name?: string;
+ /**
+ * The translucency of the image. A value of `0.0` makes the image completely
+ * transparent, and `1.0` makes the image completely opaque. Note that if the
+ * image is already semi-transparent, setting this to `1.0` will not make the
+ * image completely opaque.
+ */
+ opacity?: number;
+ /**
+ * The whitespace between the adjacent edges (determined by position) of the
+ * video and the image. `0.0` indicates no padding, and `1.0` indicates a fully
+ * padded video width or length, as determined by the algorithm.
+ */
+ padding?: number;
+ /**
+ * The size of the image relative to the overall size of the video. This
+ * parameter will adapt to horizontal and vertical videos automatically. `0.0`
+ * indicates no scaling (use the size of the image as-is), and `1.0 `fills the
+ * entire video.
+ */
+ scale?: number;
+ /**
+ * The location of the image.
+ */
+ position?: StreamWatermarkPosition;
+};
+type StreamVideosListParams = {
+ /**
+ * The maximum number of videos to return.
+ */
+ limit?: number;
+ /**
+ * Return videos created before this timestamp.
+ * (RFC3339/RFC3339Nano)
+ */
+ before?: string;
+ /**
+ * Comparison operator for the `before` field.
+ * @default 'lt'
+ */
+ beforeComp?: StreamPaginationComparison;
+ /**
+ * Return videos created after this timestamp.
+ * (RFC3339/RFC3339Nano)
+ */
+ after?: string;
+ /**
+ * Comparison operator for the `after` field.
+ * @default 'gte'
+ */
+ afterComp?: StreamPaginationComparison;
+};
+type StreamPaginationComparison = 'eq' | 'gt' | 'gte' | 'lt' | 'lte';
+/**
+ * Error object for Stream binding operations.
+ */
+interface StreamError extends Error {
+ readonly code: number;
+ readonly statusCode: number;
+ readonly message: string;
+ readonly stack?: string;
+}
+interface InternalError extends StreamError {
+ name: 'InternalError';
+}
+interface BadRequestError extends StreamError {
+ name: 'BadRequestError';
+}
+interface NotFoundError extends StreamError {
+ name: 'NotFoundError';
+}
+interface ForbiddenError extends StreamError {
+ name: 'ForbiddenError';
+}
+interface RateLimitedError extends StreamError {
+ name: 'RateLimitedError';
+}
+interface QuotaReachedError extends StreamError {
+ name: 'QuotaReachedError';
+}
+interface MaxFileSizeError extends StreamError {
+ name: 'MaxFileSizeError';
+}
+interface InvalidURLError extends StreamError {
+ name: 'InvalidURLError';
+}
+interface AlreadyUploadedError extends StreamError {
+ name: 'AlreadyUploadedError';
+}
+interface TooManyWatermarksError extends StreamError {
+ name: 'TooManyWatermarksError';
+}
type MarkdownDocument = {
name: string;
blob: Blob;
diff --git a/wrangler.toml b/wrangler.toml
index 0cf6c80..c7efc01 100644
--- a/wrangler.toml
+++ b/wrangler.toml
@@ -21,8 +21,19 @@ binding = "DB"
database_name = "bskylabels"
database_id = ""
+[[ratelimits]]
+name = "BSKY_LABEL_LIMITER"
+namespace_id = "461001"
+[ratelimits.simple]
+limit = 10
+period = 60
+
[vars]
COUNT_CACHE_HOUR="4"
+
+# use rate limits (recommended)
+RATELIMIT="true"
+
# Site metadata
SITE_TITLE="Simple BSky Labeler"
SITE_DESCRIPTION="Allow people to obtain a bsky label you provide"