Compare commits
6 commits
3fbfa40360
...
d91f1491f1
| Author | SHA1 | Date | |
|---|---|---|---|
| d91f1491f1 | |||
| ea675c8818 | |||
| 8501f0ac3a | |||
| d8f56bd942 | |||
| affaa13e4b | |||
| aa13287f67 |
22 changed files with 14783 additions and 65 deletions
6
.envrc
Normal file
6
.envrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [[ $(type -t use_flake) != function ]]; then
|
||||||
|
echo "ERROR: direnv's use_flake function missing. update direnv to v2.30.0 or later." && exit 1
|
||||||
|
fi
|
||||||
|
use flake
|
||||||
|
dotenv
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.direnv
|
||||||
|
node_modules
|
||||||
|
result
|
||||||
18
CHANGELOG.md
Normal file
18
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
Changes yet to be released are documented here.
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
Initial release.
|
||||||
|
|
||||||
|
- `nix-cache` Worker serving Nix binary cache from Cloudflare R2
|
||||||
|
- JWT authentication via Keycloak OIDC (RS256, cached JWKS)
|
||||||
|
- Group-based authorization (`nix-cache-users` claim required)
|
||||||
|
- Bearer and Basic auth support (Basic for Nix netrc compatibility)
|
||||||
|
- Range request support (single byte ranges)
|
||||||
|
- Conditional request handling (etag, if-match, if-modified-since)
|
||||||
|
- Edge caching via Cloudflare Cache API
|
||||||
|
- Nix flake with checks (vitest in sandbox) and devShell
|
||||||
41
README.md
Normal file
41
README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# cloudflare-workers
|
||||||
|
|
||||||
|
Cloudflare Workers for Guardian Project infrastructure.
|
||||||
|
|
||||||
|
Canonical Repository: https://guardianproject.dev/ops/cloudflare-workers
|
||||||
|
|
||||||
|
## Workers
|
||||||
|
|
||||||
|
- **[nix-cache](nix-cache/)** — Nix binary cache proxy backed by Cloudflare R2 with JWT authentication
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# enter dev environment
|
||||||
|
nix develop
|
||||||
|
|
||||||
|
# run all checks in the Nix sandbox
|
||||||
|
nix flake check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
This project is actively maintained by [Guardian Project](https://guardianproject.info).
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
For bug reports and feature requests, please use the [Issues][issues] page.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
For security-related issues, please contact us through our [security policy][sec].
|
||||||
|
|
||||||
|
[issues]: https://guardianproject.dev/ops/cloudflare-workers/issues
|
||||||
|
[sec]: https://guardianproject.info/contact/
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Abel Luck <abel@guardianproject.info>
|
||||||
|
|
||||||
|
This project is licensed under the GNU General Public License v3.0 or later -
|
||||||
|
see the [LICENSE](LICENSE) file for details.
|
||||||
34
flake.lock
generated
34
flake.lock
generated
|
|
@ -1,23 +1,5 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-parts": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1769996383,
|
|
||||||
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1771848320,
|
"lastModified": 1771848320,
|
||||||
|
|
@ -32,24 +14,8 @@
|
||||||
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1"
|
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1769909678,
|
|
||||||
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nixpkgs.lib",
|
|
||||||
"rev": "72716169fe93074c333e8d0173151350670b824c",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nixpkgs.lib",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
flake.nix
70
flake.nix
|
|
@ -1,31 +1,58 @@
|
||||||
{
|
{
|
||||||
inputs = {
|
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1";
|
||||||
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1"; # tracks nixpkgs unstable branch
|
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{ self, flake-parts, ... }:
|
{ self, nixpkgs }:
|
||||||
flake-parts.lib.mkFlake { inherit self; } {
|
let
|
||||||
flake = {
|
|
||||||
# Put your original flake attributes here.
|
|
||||||
};
|
|
||||||
systems = [
|
systems = [
|
||||||
# systems for which you want to build the `perSystem` attributes
|
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
"aarch64-darwin"
|
"aarch64-darwin"
|
||||||
];
|
];
|
||||||
perSystem =
|
forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system});
|
||||||
|
in
|
||||||
{
|
{
|
||||||
config,
|
packages = forAllSystems (pkgs: {
|
||||||
self',
|
nix-cache = pkgs.buildNpmPackage {
|
||||||
inputs',
|
pname = "nix-cache";
|
||||||
pkgs,
|
version = "0.1.2";
|
||||||
system,
|
src = ./nix-cache;
|
||||||
...
|
npmDepsHash = "sha256-bIujU7pZa1ExCZOMOoxsTeLDSF1GuPN/oMSgiMhY/04=";
|
||||||
}:
|
buildPhase = ''
|
||||||
{
|
npx wrangler deploy --dry-run --outdir dist
|
||||||
devShells = {
|
'';
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
cp dist/index.js $out/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
checks = forAllSystems (pkgs: {
|
||||||
|
nix-cache-tests = pkgs.buildNpmPackage {
|
||||||
|
pname = "nix-cache-tests";
|
||||||
|
version = "0.0.0";
|
||||||
|
src = ./nix-cache;
|
||||||
|
npmDepsHash = "sha256-bIujU7pZa1ExCZOMOoxsTeLDSF1GuPN/oMSgiMhY/04=";
|
||||||
|
dontBuild = true;
|
||||||
|
doCheck = true;
|
||||||
|
nativeBuildInputs = [ pkgs.patchelf ];
|
||||||
|
checkPhase = ''
|
||||||
|
runHook preCheck
|
||||||
|
binary=$(find node_modules -path '*/workerd-linux-64/bin/workerd' -type f)
|
||||||
|
if [ -n "$binary" ]; then
|
||||||
|
patchelf --set-interpreter "$(cat ${pkgs.stdenv.cc}/nix-support/dynamic-linker)" "$binary"
|
||||||
|
fi
|
||||||
|
npm test -- --run
|
||||||
|
runHook postCheck
|
||||||
|
'';
|
||||||
|
installPhase = ''
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
devShell = self.devShells.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
devShells = forAllSystems (pkgs: {
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
wrangler
|
wrangler
|
||||||
|
|
@ -35,7 +62,6 @@
|
||||||
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
nix-cache/.editorconfig
Normal file
12
nix-cache/.editorconfig
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
167
nix-cache/.gitignore
vendored
Normal file
167
nix-cache/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
\*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
\*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
\*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
\*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.\*
|
||||||
|
|
||||||
|
# wrangler project
|
||||||
|
|
||||||
|
.dev.vars*
|
||||||
|
!.dev.vars.example
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
.wrangler/
|
||||||
6
nix-cache/.prettierrc
Normal file
6
nix-cache/.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"printWidth": 140,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": true
|
||||||
|
}
|
||||||
5
nix-cache/.vscode/settings.json
vendored
Normal file
5
nix-cache/.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"wrangler.json": "jsonc"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
nix-cache/AGENTS.md
Normal file
34
nix-cache/AGENTS.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Cloudflare Workers
|
||||||
|
|
||||||
|
STOP. Your knowledge of Cloudflare Workers APIs and limits may be outdated. Always retrieve current documentation before any Workers, KV, R2, D1, Durable Objects, Queues, Vectorize, AI, or Agents SDK task.
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- https://developers.cloudflare.com/workers/
|
||||||
|
- MCP: `https://docs.mcp.cloudflare.com/mcp`
|
||||||
|
|
||||||
|
For all limits and quotas, retrieve from the product's `/platform/limits/` page. eg. `/workers/platform/limits`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `npx wrangler dev` | Local development |
|
||||||
|
| `npx wrangler deploy` | Deploy to Cloudflare |
|
||||||
|
| `npx wrangler types` | Generate TypeScript types |
|
||||||
|
|
||||||
|
Run `wrangler types` after changing bindings in wrangler.jsonc.
|
||||||
|
|
||||||
|
## Node.js Compatibility
|
||||||
|
|
||||||
|
https://developers.cloudflare.com/workers/runtime-apis/nodejs/
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
- **Error 1102** (CPU/Memory exceeded): Retrieve limits from `/workers/platform/limits/`
|
||||||
|
- **All errors**: https://developers.cloudflare.com/workers/observability/errors/
|
||||||
|
|
||||||
|
## Product Docs
|
||||||
|
|
||||||
|
Retrieve API references and limits from:
|
||||||
|
`/kv/` · `/r2/` · `/d1/` · `/durable-objects/` · `/queues/` · `/vectorize/` · `/workers-ai/` · `/agents/`
|
||||||
27
nix-cache/README.md
Normal file
27
nix-cache/README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# nix-cache
|
||||||
|
|
||||||
|
Serves a Nix binary cache from Cloudflare R2 with JWT-based authentication.
|
||||||
|
Only users with a valid Keycloak token and membership in the `nix-cache-users`
|
||||||
|
group can read from the cache.
|
||||||
|
|
||||||
|
Nix clients authenticate via netrc (Basic auth), while other clients can use
|
||||||
|
Bearer tokens directly. JWTs are verified locally using cached JWKS public keys.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # install dependencies
|
||||||
|
npm test # run vitest (uses miniflare locally)
|
||||||
|
npm run dev # start wrangler dev server on localhost:8787
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloudflare Setup
|
||||||
|
|
||||||
|
1. Create an A record on the subdomain you want this Worker to run on which
|
||||||
|
points to `192.0.2.1`
|
||||||
|
2. Edit `wrangler.jsonc`:
|
||||||
|
- `route` should be the subdomain followed by `/*`
|
||||||
|
- `bucket_name` should be the name of the R2 bucket you'll use
|
||||||
|
3. Run `npx wrangler login` to login to Wrangler
|
||||||
|
4. Run `npm run deploy`
|
||||||
|
5. Upload an `index.html` to your bucket if you want a landing page
|
||||||
2651
nix-cache/package-lock.json
generated
Normal file
2651
nix-cache/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
nix-cache/package.json
Normal file
24
nix-cache/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "nix-cache",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"start": "wrangler dev",
|
||||||
|
"test": "vitest",
|
||||||
|
"cf-typegen": "wrangler types"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/vitest-pool-workers": "^0.12.4",
|
||||||
|
"@types/node": "^25.3.1",
|
||||||
|
"@types/range-parser": "^1.2.7",
|
||||||
|
"typescript": "^5.5.2",
|
||||||
|
"vitest": "~3.2.0",
|
||||||
|
"wrangler": "^4.68.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^6.1.3",
|
||||||
|
"range-parser": "^1.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
239
nix-cache/src/index.ts
Normal file
239
nix-cache/src/index.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
import parseRange from 'range-parser';
|
||||||
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||||
|
|
||||||
|
interface Env {
|
||||||
|
R2_BUCKET: R2Bucket;
|
||||||
|
CACHE_CONTROL: string;
|
||||||
|
KEYCLOAK_ISSUER: string;
|
||||||
|
KEYCLOAK_AUDIENCE: string;
|
||||||
|
REQUIRED_GROUP: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBody(object: R2Object | R2ObjectBody): object is R2ObjectBody {
|
||||||
|
return (<R2ObjectBody>object).body !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedKeyHeader = 'x-resolved-r2-key';
|
||||||
|
|
||||||
|
type Range = { offset: number; length?: number } | { offset?: number; length: number };
|
||||||
|
|
||||||
|
function rangeToHeader(range: Range | undefined, file: R2Object | R2ObjectBody): string {
|
||||||
|
if (range) {
|
||||||
|
const offset = range.offset ?? 0;
|
||||||
|
const length = range.length ?? 0;
|
||||||
|
return `bytes ${offset}-${offset + length - 1}/${file.size}`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let JWKS: ReturnType<typeof createRemoteJWKSet> | null = null;
|
||||||
|
|
||||||
|
function getJWKS(issuer: string) {
|
||||||
|
if (!JWKS) {
|
||||||
|
const url = new URL(`${issuer}/protocol/openid-connect/certs`);
|
||||||
|
JWKS = createRemoteJWKSet(url);
|
||||||
|
}
|
||||||
|
return JWKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticate(request: Request, env: Env): Promise<boolean> {
|
||||||
|
const authHeader = request.headers.get('Authorization');
|
||||||
|
if (!authHeader) return false;
|
||||||
|
|
||||||
|
let token: string | null = null;
|
||||||
|
|
||||||
|
if (authHeader.startsWith('Bearer ')) {
|
||||||
|
token = authHeader.slice(7);
|
||||||
|
} else if (authHeader.startsWith('Basic ')) {
|
||||||
|
try {
|
||||||
|
const decoded = atob(authHeader.slice(6));
|
||||||
|
const colon = decoded.indexOf(':');
|
||||||
|
token = colon !== -1 ? decoded.slice(colon + 1) : decoded;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jwks = getJWKS(env.KEYCLOAK_ISSUER);
|
||||||
|
const { payload } = await jwtVerify(token, jwks, {
|
||||||
|
issuer: env.KEYCLOAK_ISSUER,
|
||||||
|
audience: env.KEYCLOAK_AUDIENCE,
|
||||||
|
algorithms: ['RS256'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = payload.groups;
|
||||||
|
if (!Array.isArray(groups) || !groups.includes(env.REQUIRED_GROUP)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
|
const allowedMethods = ['GET', 'HEAD', 'OPTIONS'];
|
||||||
|
if (allowedMethods.indexOf(request.method) === -1) return new Response('Method Not Allowed', { status: 405 });
|
||||||
|
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: { allow: allowedMethods.join(', ') } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.protocol !== 'https:') {
|
||||||
|
url.protocol = 'https:';
|
||||||
|
return Response.redirect(url.toString(), 301);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticated = await authenticate(request, env);
|
||||||
|
if (!authenticated) {
|
||||||
|
return new Response('Unauthorized', {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'WWW-Authenticate': 'Bearer realm="nix-cache"' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/') {
|
||||||
|
url.pathname = '/index.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = caches.default;
|
||||||
|
let response = await cache.match(request);
|
||||||
|
let range: Range | undefined;
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
console.warn('Cache miss');
|
||||||
|
const path = decodeURIComponent(url.pathname.substring(1));
|
||||||
|
|
||||||
|
let file: R2Object | R2ObjectBody | null | undefined;
|
||||||
|
|
||||||
|
// Range handling
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
const rangeHeader = request.headers.get('range');
|
||||||
|
if (rangeHeader) {
|
||||||
|
file = await env.R2_BUCKET.head(path);
|
||||||
|
if (file === null)
|
||||||
|
return new Response('File Not Found', {
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
[resolvedKeyHeader]: path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const parsedRanges = parseRange(file.size, rangeHeader);
|
||||||
|
// R2 only supports 1 range at the moment, reject if there is more than one
|
||||||
|
if (parsedRanges !== -1 && parsedRanges !== -2 && parsedRanges.length === 1 && parsedRanges.type === 'bytes') {
|
||||||
|
let firstRange = parsedRanges[0];
|
||||||
|
range = {
|
||||||
|
offset: firstRange.start,
|
||||||
|
length: firstRange.end - firstRange.start + 1,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return new Response('Range Not Satisfiable', {
|
||||||
|
status: 416,
|
||||||
|
headers: {
|
||||||
|
[resolvedKeyHeader]: path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etag/If-(Not)-Match handling
|
||||||
|
// R2 requires that etag checks must not contain quotes, and the S3 spec only allows one etag
|
||||||
|
// This silently ignores invalid or weak (W/) headers
|
||||||
|
const getHeaderEtag = (header: string | null) => header?.trim().replace(/^['"]|['"]$/g, '');
|
||||||
|
const ifMatch = getHeaderEtag(request.headers.get('if-match'));
|
||||||
|
const ifNoneMatch = getHeaderEtag(request.headers.get('if-none-match'));
|
||||||
|
|
||||||
|
const ifModifiedSince = Date.parse(request.headers.get('if-modified-since') || '');
|
||||||
|
const ifUnmodifiedSince = Date.parse(request.headers.get('if-unmodified-since') || '');
|
||||||
|
|
||||||
|
const ifRange = request.headers.get('if-range');
|
||||||
|
if (range && ifRange && file) {
|
||||||
|
const maybeDate = Date.parse(ifRange);
|
||||||
|
|
||||||
|
if (isNaN(maybeDate) || new Date(maybeDate) > file.uploaded) {
|
||||||
|
// httpEtag already has quotes, no need to use getHeaderEtag
|
||||||
|
if (ifRange !== file.httpEtag) range = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifMatch || ifUnmodifiedSince) {
|
||||||
|
file = await env.R2_BUCKET.get(path, {
|
||||||
|
onlyIf: {
|
||||||
|
etagMatches: ifMatch,
|
||||||
|
uploadedBefore: ifUnmodifiedSince ? new Date(ifUnmodifiedSince) : undefined,
|
||||||
|
},
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file && !hasBody(file)) {
|
||||||
|
return new Response('Precondition Failed', {
|
||||||
|
status: 412,
|
||||||
|
headers: {
|
||||||
|
[resolvedKeyHeader]: path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifNoneMatch || ifModifiedSince) {
|
||||||
|
// if-none-match overrides if-modified-since completely
|
||||||
|
if (ifNoneMatch) {
|
||||||
|
file = await env.R2_BUCKET.get(path, { onlyIf: { etagDoesNotMatch: ifNoneMatch }, range });
|
||||||
|
} else if (ifModifiedSince) {
|
||||||
|
file = await env.R2_BUCKET.get(path, { onlyIf: { uploadedAfter: new Date(ifModifiedSince) }, range });
|
||||||
|
}
|
||||||
|
if (file && !hasBody(file)) {
|
||||||
|
return new Response(null, { status: 304 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file =
|
||||||
|
request.method === 'HEAD'
|
||||||
|
? await env.R2_BUCKET.head(path)
|
||||||
|
: file && hasBody(file)
|
||||||
|
? file
|
||||||
|
: await env.R2_BUCKET.get(path, { range });
|
||||||
|
|
||||||
|
if (file === null) {
|
||||||
|
return new Response('File Not Found', {
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
[resolvedKeyHeader]: path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response = new Response(hasBody(file) ? file.body : null, {
|
||||||
|
status: (file?.size || 0) === 0 ? 204 : range ? 206 : 200,
|
||||||
|
headers: {
|
||||||
|
'accept-ranges': 'bytes',
|
||||||
|
|
||||||
|
etag: file.httpEtag,
|
||||||
|
'cache-control': file.httpMetadata.cacheControl ?? (env.CACHE_CONTROL || ''),
|
||||||
|
expires: file.httpMetadata.cacheExpiry?.toUTCString() ?? '',
|
||||||
|
'last-modified': file.uploaded.toUTCString(),
|
||||||
|
|
||||||
|
'content-encoding': file.httpMetadata?.contentEncoding ?? '',
|
||||||
|
'content-type': file.httpMetadata?.contentType ?? 'application/octet-stream',
|
||||||
|
'content-language': file.httpMetadata?.contentLanguage ?? '',
|
||||||
|
'content-disposition': file.httpMetadata?.contentDisposition ?? '',
|
||||||
|
'content-range': rangeToHeader(range, file),
|
||||||
|
|
||||||
|
[resolvedKeyHeader]: path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === 'GET' && !range) ctx.waitUntil(cache.put(request, response.clone()));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
} satisfies ExportedHandler<Env>;
|
||||||
3
nix-cache/test/env.d.ts
vendored
Normal file
3
nix-cache/test/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'cloudflare:test' {
|
||||||
|
interface ProvidedEnv extends Env {}
|
||||||
|
}
|
||||||
159
nix-cache/test/index.spec.ts
Normal file
159
nix-cache/test/index.spec.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { env, createExecutionContext, fetchMock } from 'cloudflare:test';
|
||||||
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
|
import { generateKeyPair, SignJWT, exportJWK } from 'jose';
|
||||||
|
import worker from '../src/index';
|
||||||
|
|
||||||
|
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
|
||||||
|
|
||||||
|
let privateKey: CryptoKey;
|
||||||
|
let jwksResponse: object;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const { publicKey, privateKey: pk } = await generateKeyPair('RS256');
|
||||||
|
privateKey = pk;
|
||||||
|
|
||||||
|
const publicJwk = await exportJWK(publicKey);
|
||||||
|
publicJwk.alg = 'RS256';
|
||||||
|
publicJwk.use = 'sig';
|
||||||
|
publicJwk.kid = 'test-key';
|
||||||
|
jwksResponse = { keys: [publicJwk] };
|
||||||
|
|
||||||
|
fetchMock.activate();
|
||||||
|
fetchMock.disableNetConnect();
|
||||||
|
|
||||||
|
fetchMock
|
||||||
|
.get('https://id.guardianproject.info')
|
||||||
|
.intercept({ path: '/realms/gp/protocol/openid-connect/certs' })
|
||||||
|
.reply(200, JSON.stringify(jwksResponse), {
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
})
|
||||||
|
.persist();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function makeJWT(overrides: Record<string, unknown> = {}): Promise<string> {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const defaults = {
|
||||||
|
iss: env.KEYCLOAK_ISSUER,
|
||||||
|
aud: env.KEYCLOAK_AUDIENCE,
|
||||||
|
sub: 'test-user',
|
||||||
|
groups: [env.REQUIRED_GROUP],
|
||||||
|
iat: now,
|
||||||
|
exp: now + 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
const claims = { ...defaults, ...overrides };
|
||||||
|
|
||||||
|
let builder = new SignJWT(claims)
|
||||||
|
.setProtectedHeader({ alg: 'RS256', kid: 'test-key' });
|
||||||
|
|
||||||
|
if (claims.iss !== undefined) builder = builder.setIssuer(claims.iss as string);
|
||||||
|
if (claims.aud !== undefined) builder = builder.setAudience(claims.aud as string);
|
||||||
|
if (claims.exp !== undefined) builder = builder.setExpirationTime(claims.exp as number);
|
||||||
|
|
||||||
|
return builder.sign(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('nix-cache auth', () => {
|
||||||
|
it('HTTP request redirects to HTTPS', async () => {
|
||||||
|
const request = new IncomingRequest('http://example.com/nix-cache-info');
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(301);
|
||||||
|
expect(response.headers.get('location')).toBe('https://example.com/nix-cache-info');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('OPTIONS without auth returns 200', async () => {
|
||||||
|
const request = new IncomingRequest('https://example.com/', { method: 'OPTIONS' });
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with no Authorization header returns 401', async () => {
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info');
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.headers.get('WWW-Authenticate')).toBe('Bearer realm="nix-cache"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with unrecognized auth scheme returns 401', async () => {
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: 'Token xyz' },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with Bearer + valid JWT passes auth', async () => {
|
||||||
|
const token = await makeJWT();
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).not.toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with Basic auth :token format passes auth', async () => {
|
||||||
|
const token = await makeJWT();
|
||||||
|
const basic = btoa(':' + token);
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: `Basic ${basic}` },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).not.toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with Basic auth user:token format passes auth', async () => {
|
||||||
|
const token = await makeJWT();
|
||||||
|
const basic = btoa('user:' + token);
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: `Basic ${basic}` },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).not.toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with invalid Bearer token returns 401', async () => {
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: 'Bearer garbage-token' },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with expired JWT returns 401', async () => {
|
||||||
|
const token = await makeJWT({ exp: Math.floor(Date.now() / 1000) - 3600 });
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with JWT missing groups claim returns 401', async () => {
|
||||||
|
const token = await makeJWT({ groups: undefined });
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET with JWT where groups does not contain required group returns 401', async () => {
|
||||||
|
const token = await makeJWT({ groups: ['some-other-group'] });
|
||||||
|
const request = new IncomingRequest('https://example.com/nix-cache-info', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const ctx = createExecutionContext();
|
||||||
|
const response = await worker.fetch(request, env, ctx);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
8
nix-cache/test/tsconfig.json
Normal file
8
nix-cache/test/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["@cloudflare/vitest-pool-workers"]
|
||||||
|
},
|
||||||
|
"include": ["./**/*.ts", "../worker-configuration.d.ts"],
|
||||||
|
"exclude": []
|
||||||
|
}
|
||||||
46
nix-cache/tsconfig.json
Normal file
46
nix-cache/tsconfig.json
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
"target": "es2024",
|
||||||
|
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
"lib": ["es2024"],
|
||||||
|
/* Specify what JSX code is generated. */
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Specify what module code is generated. */
|
||||||
|
"module": "es2022",
|
||||||
|
/* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
/* Enable importing .json files */
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||||
|
"allowJs": true,
|
||||||
|
/* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
"checkJs": false,
|
||||||
|
|
||||||
|
/* Disable emitting files from a compilation. */
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
"isolatedModules": true,
|
||||||
|
/* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
/* Ensure that casing is correct in imports. */
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
/* Enable all strict type-checking options. */
|
||||||
|
"strict": true,
|
||||||
|
|
||||||
|
/* Skip type checking all .d.ts files. */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": [
|
||||||
|
"./worker-configuration.d.ts",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": ["test"],
|
||||||
|
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
|
||||||
|
}
|
||||||
11
nix-cache/vitest.config.mts
Normal file
11
nix-cache/vitest.config.mts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
|
||||||
|
|
||||||
|
export default defineWorkersConfig({
|
||||||
|
test: {
|
||||||
|
poolOptions: {
|
||||||
|
workers: {
|
||||||
|
wrangler: { configPath: './wrangler.jsonc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
11214
nix-cache/worker-configuration.d.ts
vendored
Normal file
11214
nix-cache/worker-configuration.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
52
nix-cache/wrangler.jsonc
Normal file
52
nix-cache/wrangler.jsonc
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* For more details on how to configure Wrangler, refer to:
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "nix-cache",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2026-02-26",
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"compatibility_flags": [
|
||||||
|
"nodejs_compat"
|
||||||
|
],
|
||||||
|
"vars": {
|
||||||
|
"KEYCLOAK_ISSUER": "https://id.guardianproject.info/realms/gp",
|
||||||
|
"KEYCLOAK_AUDIENCE": "nix-cache",
|
||||||
|
"REQUIRED_GROUP": "nix-cache-users"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{ "binding": "R2_BUCKET", "bucket_name": "nix-cache" }
|
||||||
|
]
|
||||||
|
/**
|
||||||
|
* Smart Placement
|
||||||
|
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||||
|
*/
|
||||||
|
// "placement": { "mode": "smart" }
|
||||||
|
/**
|
||||||
|
* Bindings
|
||||||
|
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||||
|
* databases, object storage, AI inference, real-time communication and more.
|
||||||
|
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Environment Variables
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||||
|
* Note: Use secrets to store sensitive data.
|
||||||
|
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||||
|
*/
|
||||||
|
// "vars": { "MY_VARIABLE": "production_value" }
|
||||||
|
/**
|
||||||
|
* Static Assets
|
||||||
|
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||||
|
*/
|
||||||
|
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
||||||
|
/**
|
||||||
|
* Service Bindings (communicate between multiple Workers)
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||||
|
*/
|
||||||
|
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue