Add Better Auth worker route

This commit is contained in:
2026-05-09 03:35:00 -04:00
parent 928bb3cfb0
commit 0178940e0f
13 changed files with 556 additions and 4 deletions
+19
View File
@@ -21,3 +21,22 @@ scripts/verify_prd_site_rendering.sh
scripts/verify_windows_app_manifest.sh scripts/verify_windows_app_manifest.sh
cargo run -p ely_app cargo run -p ely_app
``` ```
## Cloudflare Auth Configuration
`/api/auth/*` is served by Better Auth in the Cloudflare Worker. Local `wrangler dev`
uses `ELY_AUTH_BASE_URL` from `cloudflare/wrangler.toml`; deployed environments should
set the matching public Worker origin.
Use Wrangler secrets or an untracked `cloudflare/.dev.vars` file for the remaining auth
bindings:
```bash
wrangler secret put ELY_AUTH_SECRET
wrangler secret put ELY_AUTH_GOOGLE_CLIENT_ID
wrangler secret put ELY_AUTH_GOOGLE_CLIENT_SECRET
wrangler secret put ELY_AUTH_GITHUB_CLIENT_ID
wrangler secret put ELY_AUTH_GITHUB_CLIENT_SECRET
wrangler secret put ELY_AUTH_EMAIL_OTP_ENDPOINT
wrangler secret put ELY_AUTH_EMAIL_OTP_TOKEN
```
+360 -1
View File
@@ -7,6 +7,9 @@
"": { "": {
"name": "@elydora/cloudflare-worker", "name": "@elydora/cloudflare-worker",
"version": "0.1.0", "version": "0.1.0",
"dependencies": {
"better-auth": "^1.6.9"
},
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "4.20260508.1", "@cloudflare/workers-types": "4.20260508.1",
"@types/node": "24.12.3", "@types/node": "24.12.3",
@@ -14,6 +17,138 @@
"wrangler": "4.90.0" "wrangler": "4.90.0"
} }
}, },
"node_modules/@better-auth/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.9.tgz",
"integrity": "sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w==",
"license": "MIT",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.39.0",
"@standard-schema/spec": "^1.1.0",
"zod": "^4.3.6"
},
"peerDependencies": {
"@better-auth/utils": "0.4.0",
"@better-fetch/fetch": "1.1.21",
"@cloudflare/workers-types": ">=4",
"@opentelemetry/api": "^1.9.0",
"better-call": "1.3.5",
"jose": "^6.1.0",
"kysely": "^0.28.5",
"nanostores": "^1.0.1"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
}
}
},
"node_modules/@better-auth/drizzle-adapter": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.9.tgz",
"integrity": "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.9",
"@better-auth/utils": "0.4.0",
"drizzle-orm": "^0.45.2"
},
"peerDependenciesMeta": {
"drizzle-orm": {
"optional": true
}
}
},
"node_modules/@better-auth/kysely-adapter": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.9.tgz",
"integrity": "sha512-gyjuuxJtZ4o9G9z9q4kqn24X2kvMSp7F+KHogYxF03SnXY/2WleAcuj57iC4wP3e9mGDbjPOrnM5K6Kr3Ktdpw==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.9",
"@better-auth/utils": "0.4.0",
"kysely": "^0.28.14"
},
"peerDependenciesMeta": {
"kysely": {
"optional": true
}
}
},
"node_modules/@better-auth/memory-adapter": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.9.tgz",
"integrity": "sha512-XmIG4tUnOXZ+KEcWjHUjOI9Z5donD09dC2t/AQTXifAUIqx7cySg86w0KTM09ArzAxRx1fCqO36Wkt5nULnrkQ==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.9",
"@better-auth/utils": "0.4.0"
}
},
"node_modules/@better-auth/mongo-adapter": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.9.tgz",
"integrity": "sha512-h+AiRJ/TsBSi+ZDjySASBpbJ/9QCXBre34PSKgCz7QmTHrFM9Cg2EM4AM7LjR5lPXipEE+2rWPBc9wfnUBjhcw==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.9",
"@better-auth/utils": "0.4.0",
"mongodb": "^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
}
}
},
"node_modules/@better-auth/prisma-adapter": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.9.tgz",
"integrity": "sha512-XHks01ntK20orqK/jICq8wmEbJ/zT6dct49Fk8zTQKN9QNGDc+Ix5+7z/Kvui0DXGFf790GfvRozquzaLtXa8Q==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.9",
"@better-auth/utils": "0.4.0",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"@prisma/client": {
"optional": true
},
"prisma": {
"optional": true
}
}
},
"node_modules/@better-auth/telemetry": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.9.tgz",
"integrity": "sha512-0u5zkhSCAQFoN3DHvUkLHOF6MBbVTDAa6mU8mhPwiysdz1x21vMzhzfaAKN/ZGWaQ09v91/F+2qu42G/bhUV4A==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "^1.6.9",
"@better-auth/utils": "0.4.0",
"@better-fetch/fetch": "1.1.21"
}
},
"node_modules/@better-auth/utils": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz",
"integrity": "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^2.0.1"
}
},
"node_modules/@better-fetch/fetch": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
"integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
},
"node_modules/@cloudflare/kv-asset-handler": { "node_modules/@cloudflare/kv-asset-handler": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
@@ -129,7 +264,7 @@
"version": "4.20260508.1", "version": "4.20260508.1",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260508.1.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260508.1.tgz",
"integrity": "sha512-0KNR+UkrYJYmtyQ5tOjUT/wt/U34FuE4Y8FLSbPMwFrGQQpmvR9wKghwRkL4d28y5t9jI9cvGqeAoo/cerTnCQ==", "integrity": "sha512-0KNR+UkrYJYmtyQ5tOjUT/wt/U34FuE4Y8FLSbPMwFrGQQpmvR9wKghwRkL4d28y5t9jI9cvGqeAoo/cerTnCQ==",
"dev": true, "devOptional": true,
"license": "MIT OR Apache-2.0" "license": "MIT OR Apache-2.0"
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@@ -1164,6 +1299,39 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@noble/ciphers": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz",
"integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
"integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@poppinss/colors": { "node_modules/@poppinss/colors": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
@@ -1213,6 +1381,12 @@
"dev": true, "dev": true,
"license": "CC0-1.0" "license": "CC0-1.0"
}, },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.12.3", "version": "24.12.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.3.tgz",
@@ -1223,6 +1397,131 @@
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/better-auth": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.9.tgz",
"integrity": "sha512-EBFURtglyiEZxbx4NJBoqUD8J65dX24yC+6I9AUbIXNgUkt76mshzGbHkxZ3n/lB7Dwq3kBC+hHt0hUQsnL7HA==",
"license": "MIT",
"dependencies": {
"@better-auth/core": "1.6.9",
"@better-auth/drizzle-adapter": "1.6.9",
"@better-auth/kysely-adapter": "1.6.9",
"@better-auth/memory-adapter": "1.6.9",
"@better-auth/mongo-adapter": "1.6.9",
"@better-auth/prisma-adapter": "1.6.9",
"@better-auth/telemetry": "1.6.9",
"@better-auth/utils": "0.4.0",
"@better-fetch/fetch": "1.1.21",
"@noble/ciphers": "^2.1.1",
"@noble/hashes": "^2.0.1",
"better-call": "1.3.5",
"defu": "^6.1.4",
"jose": "^6.1.3",
"kysely": "^0.28.14",
"nanostores": "^1.1.1",
"zod": "^4.3.6"
},
"peerDependencies": {
"@lynx-js/react": "*",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"@sveltejs/kit": "^2.0.0",
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
"drizzle-kit": ">=0.31.4",
"drizzle-orm": "^0.45.2",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
"pg": "^8.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"solid-js": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0",
"vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@lynx-js/react": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"@tanstack/solid-start": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"drizzle-kit": {
"optional": true
},
"drizzle-orm": {
"optional": true
},
"mongodb": {
"optional": true
},
"mysql2": {
"optional": true
},
"next": {
"optional": true
},
"pg": {
"optional": true
},
"prisma": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vitest": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/better-call": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz",
"integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "^0.4.0",
"@better-fetch/fetch": "^1.1.21",
"rou3": "^0.7.12",
"set-cookie-parser": "^3.0.1"
},
"peerDependencies": {
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/blake3-wasm": { "node_modules/blake3-wasm": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
@@ -1244,6 +1543,12 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1321,6 +1626,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/jose": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -1331,6 +1645,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/kysely": {
"version": "0.28.17",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz",
"integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/miniflare": { "node_modules/miniflare": {
"version": "4.20260507.1", "version": "4.20260507.1",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260507.1.tgz", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260507.1.tgz",
@@ -1352,6 +1675,21 @@
"node": ">=22.0.0" "node": ">=22.0.0"
} }
}, },
"node_modules/nanostores": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.3.0.tgz",
"integrity": "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
@@ -1366,6 +1704,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/rou3": {
"version": "0.7.12",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
"integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
"license": "MIT"
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.4", "version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -1379,6 +1723,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
"license": "MIT"
},
"node_modules/sharp": { "node_modules/sharp": {
"version": "0.34.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -1588,6 +1938,15 @@
"@poppinss/exception": "^1.2.2", "@poppinss/exception": "^1.2.2",
"error-stack-parser-es": "^1.0.5" "error-stack-parser-es": "^1.0.5"
} }
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }
+3
View File
@@ -12,5 +12,8 @@
"@types/node": "24.12.3", "@types/node": "24.12.3",
"typescript": "6.0.3", "typescript": "6.0.3",
"wrangler": "4.90.0" "wrangler": "4.90.0"
},
"dependencies": {
"better-auth": "^1.6.9"
} }
} }
+137
View File
@@ -0,0 +1,137 @@
import { betterAuth, type BetterAuthOptions } from "better-auth";
import { emailOTP } from "better-auth/plugins/email-otp";
import type { Env } from "./bindings.js";
import { jsonResponse } from "./responses.js";
const APP_NAME = "ELY Browser";
const AUTH_BASE_PATH = "/api/auth";
const AUTH_CALLBACK_URL = "ely://auth/callback";
const OTP_REQUEST_TIMEOUT_MS = 10_000;
type BetterAuthDatabase = NonNullable<BetterAuthOptions["database"]>;
type BetterAuthSocialProviders = NonNullable<BetterAuthOptions["socialProviders"]>;
export async function handleBetterAuthRoute(request: Request, env: Env): Promise<Response> {
try {
return createElyAuth(env).handler(request);
} catch (error) {
if (error instanceof BetterAuthConfigError) {
return jsonResponse({ error: "auth_unconfigured" }, 500, {
"Cache-Control": "no-store",
});
}
throw error;
}
}
function createElyAuth(env: Env) {
return betterAuth({
appName: APP_NAME,
basePath: AUTH_BASE_PATH,
baseURL: requiredBinding(env.ELY_AUTH_BASE_URL, "ELY_AUTH_BASE_URL"),
secret: requiredBinding(env.ELY_AUTH_SECRET, "ELY_AUTH_SECRET"),
database: env.ELY_DB as BetterAuthDatabase,
emailAndPassword: {
enabled: true,
minPasswordLength: 12,
maxPasswordLength: 128,
revokeSessionsOnPasswordReset: true,
},
socialProviders: socialProviders(env),
trustedOrigins: [requiredBinding(env.ELY_AUTH_BASE_URL, "ELY_AUTH_BASE_URL"), AUTH_CALLBACK_URL],
plugins: [
emailOTP({
expiresIn: 300,
allowedAttempts: 3,
storeOTP: "encrypted",
resendStrategy: "rotate",
sendVerificationOTP: (data) => sendVerificationOtp(env, data),
}),
],
});
}
function socialProviders(env: Env): BetterAuthSocialProviders {
const providers: BetterAuthSocialProviders = {};
const google = bindingPair(env.ELY_AUTH_GOOGLE_CLIENT_ID, env.ELY_AUTH_GOOGLE_CLIENT_SECRET);
if (google !== null) {
providers.google = {
clientId: google.clientId,
clientSecret: google.clientSecret,
};
}
const github = bindingPair(env.ELY_AUTH_GITHUB_CLIENT_ID, env.ELY_AUTH_GITHUB_CLIENT_SECRET);
if (github !== null) {
providers.github = {
clientId: github.clientId,
clientSecret: github.clientSecret,
};
}
return providers;
}
function bindingPair(
clientId: string | undefined,
clientSecret: string | undefined,
): { clientId: string; clientSecret: string } | null {
if (!isPresent(clientId) || !isPresent(clientSecret)) {
return null;
}
return { clientId: clientId.trim(), clientSecret: clientSecret.trim() };
}
async function sendVerificationOtp(
env: Env,
data: {
email: string;
otp: string;
type: "sign-in" | "email-verification" | "forget-password" | "change-email";
},
): Promise<void> {
const endpoint = requiredBinding(env.ELY_AUTH_EMAIL_OTP_ENDPOINT, "ELY_AUTH_EMAIL_OTP_ENDPOINT");
const token = requiredBinding(env.ELY_AUTH_EMAIL_OTP_TOKEN, "ELY_AUTH_EMAIL_OTP_TOKEN");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), OTP_REQUEST_TIMEOUT_MS);
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
signal: controller.signal,
});
if (!response.ok) {
throw new BetterAuthEmailDeliveryError(response.status);
}
} finally {
clearTimeout(timeout);
}
}
function requiredBinding(value: string | undefined, name: string): string {
if (isPresent(value)) {
return value.trim();
}
throw new BetterAuthConfigError(name);
}
function isPresent(value: string | undefined): value is string {
return value !== undefined && value.trim() !== "";
}
class BetterAuthConfigError extends Error {
constructor(readonly binding: string) {
super(`missing auth binding: ${binding}`);
this.name = "BetterAuthConfigError";
}
}
class BetterAuthEmailDeliveryError extends Error {
constructor(readonly status: number) {
super(`auth otp delivery failed with status ${status}`);
this.name = "BetterAuthEmailDeliveryError";
}
}
+8
View File
@@ -52,4 +52,12 @@ export interface Env {
ELY_RATE_LIMITER: ElyRateLimit; ELY_RATE_LIMITER: ElyRateLimit;
ELY_API_AUDIT: ElyAnalyticsDataset; ELY_API_AUDIT: ElyAnalyticsDataset;
ELY_ENVIRONMENT: string; ELY_ENVIRONMENT: string;
ELY_AUTH_BASE_URL: string;
ELY_AUTH_SECRET: string;
ELY_AUTH_GOOGLE_CLIENT_ID?: string;
ELY_AUTH_GOOGLE_CLIENT_SECRET?: string;
ELY_AUTH_GITHUB_CLIENT_ID?: string;
ELY_AUTH_GITHUB_CLIENT_SECRET?: string;
ELY_AUTH_EMAIL_OTP_ENDPOINT?: string;
ELY_AUTH_EMAIL_OTP_TOKEN?: string;
} }
+4
View File
@@ -1,5 +1,6 @@
import type { Env } from "./bindings.js"; import type { Env } from "./bindings.js";
import { withAuthenticatedApiControls, withPublicApiControls } from "./api_controls.js"; import { withAuthenticatedApiControls, withPublicApiControls } from "./api_controls.js";
import { handleBetterAuthRoute } from "./better_auth.js";
import { import {
DevicePermissionError, DevicePermissionError,
DevicePersistenceError, DevicePersistenceError,
@@ -42,6 +43,9 @@ export default {
export async function handleRequest(request: Request, env: Env): Promise<Response> { export async function handleRequest(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
if (url.pathname === "/api/auth" || url.pathname.startsWith("/api/auth/")) {
return handleBetterAuthRoute(request, env);
}
if (url.pathname === "/api/devices") { if (url.pathname === "/api/devices") {
return withAuthenticatedApiControls(request, env, "devices.list", ["GET"], async (context) => { return withAuthenticatedApiControls(request, env, "devices.list", ["GET"], async (context) => {
try { try {
+3
View File
@@ -0,0 +1,3 @@
declare module "bun:sqlite" {
export class Database {}
}
+2
View File
@@ -244,6 +244,8 @@ function testEnv(options: TestEnvOptions = {}): Env {
return { return {
ELY_ENVIRONMENT: "local", ELY_ENVIRONMENT: "local",
ELY_AUTH_BASE_URL: "https://elydora.test",
ELY_AUTH_SECRET: "test-auth-secret-for-api-controls",
ELY_DB: testD1Database(), ELY_DB: testD1Database(),
ELY_KV: { ELY_KV: {
get(key: string): Promise<string | null> { get(key: string): Promise<string | null> {
+2
View File
@@ -41,6 +41,8 @@ export function testEnv(options: TestEnvOptions): Env {
const values = new Map(options.kvEntries ?? []); const values = new Map(options.kvEntries ?? []);
return { return {
ELY_ENVIRONMENT: "local", ELY_ENVIRONMENT: "local",
ELY_AUTH_BASE_URL: "https://elydora.test",
ELY_AUTH_SECRET: "test-auth-secret-for-worker-routes",
ELY_DB: options.d1 ?? testD1Database([]), ELY_DB: options.d1 ?? testD1Database([]),
ELY_KV: { ELY_KV: {
get(key: string): Promise<string | null> { get(key: string): Promise<string | null> {
+13
View File
@@ -82,6 +82,17 @@ describe("worker routes", () => {
assert.deepEqual(await response.json(), { error: "not_found" }); assert.deepEqual(await response.json(), { error: "not_found" });
}); });
it("routes Better Auth session requests under api auth", async () => {
const response = await handleRequest(
new Request("https://elydora.test/api/auth/get-session"),
testEnv(null),
);
assert.equal(response.status, 200);
assert.equal(response.headers.get("content-type"), "application/json");
assert.equal(await response.text(), "null");
});
it("returns public plugin catalog from KV", async () => { it("returns public plugin catalog from KV", async () => {
const response = await handleRequest( const response = await handleRequest(
new Request("https://elydora.test/api/plugins"), new Request("https://elydora.test/api/plugins"),
@@ -341,6 +352,8 @@ function testEnv(
return { return {
ELY_ENVIRONMENT: "local", ELY_ENVIRONMENT: "local",
ELY_AUTH_BASE_URL: "https://elydora.test",
ELY_AUTH_SECRET: "test-auth-secret-for-worker-routes",
ELY_DB: testD1Database(), ELY_DB: testD1Database(),
ELY_KV: { ELY_KV: {
get(key: string): Promise<string | null> { get(key: string): Promise<string | null> {
+2 -2
View File
@@ -4,12 +4,12 @@
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"lib": ["ES2022"], "lib": ["ES2022"],
"types": ["@cloudflare/workers-types"], "types": ["@cloudflare/workers-types", "node"],
"strict": true, "strict": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true, "exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": false, "skipLibCheck": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"rootDir": "." "rootDir": "."
}, },
+1 -1
View File
@@ -9,7 +9,7 @@
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true, "exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": false, "skipLibCheck": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"rootDir": "." "rootDir": "."
}, },
+2
View File
@@ -1,6 +1,7 @@
name = "ely-browser-cloud" name = "ely-browser-cloud"
main = "src/index.ts" main = "src/index.ts"
compatibility_date = "2026-05-08" compatibility_date = "2026-05-08"
compatibility_flags = ["nodejs_als"]
[[d1_databases]] [[d1_databases]]
binding = "ELY_DB" binding = "ELY_DB"
@@ -18,6 +19,7 @@ id = "5eff92ba31c94fcf83e1b6d5e79ce070"
[vars] [vars]
ELY_ENVIRONMENT = "local" ELY_ENVIRONMENT = "local"
ELY_AUTH_BASE_URL = "http://localhost:8787"
[[analytics_engine_datasets]] [[analytics_engine_datasets]]
binding = "ELY_API_AUDIT" binding = "ELY_API_AUDIT"