Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support cloudflare access #348

Merged
merged 13 commits into from
Oct 29, 2024
10 changes: 9 additions & 1 deletion src/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface HubConfig {
userToken?: string
env?: string
version?: string
cloudflareAccess?: {
clientId: string
clientSecret: string
}

ai?: boolean
analytics?: boolean
Expand Down Expand Up @@ -332,7 +336,11 @@ export async function setupRemote(_nuxt: Nuxt, hub: HubConfig) {
const remoteManifest = hub.remoteManifest = await $fetch<HubConfig['remoteManifest']>('/api/_hub/manifest', {
baseURL: hub.projectUrl as string,
headers: {
authorization: `Bearer ${hub.projectSecretKey || hub.userToken}`
authorization: `Bearer ${hub.projectSecretKey || hub.userToken}`,
...(hub.cloudflareAccess?.clientId && hub.cloudflareAccess?.clientSecret && {
'CF-Access-Client-Id': hub.cloudflareAccess?.clientId,
'CF-Access-Client-Secret': hub.cloudflareAccess?.clientSecret
})
}
})
.catch(async (err) => {
Expand Down
5 changes: 5 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export default defineNuxtModule<ModuleOptions>({
hyperdrive: {},
// @ts-expect-error nitro.cloudflare.wrangler is not yet typed
compatibilityFlags: nuxt.options.nitro.cloudflare?.wrangler?.compatibility_flags
},
// Cloudflare Access
cloudflareAccess: {
clientId: process.env.NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID || null,
clientSecret: process.env.NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET || null
}
})
runtimeConfig.hub = hub
Expand Down
43 changes: 41 additions & 2 deletions src/runtime/ai/server/utils/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createError } from 'h3'
import type { H3Error } from 'h3'
import type { Ai } from '@cloudflare/workers-types/experimental'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

let _ai: Ai
Expand Down Expand Up @@ -63,6 +64,42 @@ export function hubAI(): Ai {
}
if (!_ai) {
throw createError('Missing Cloudflare AI binding (AI)')
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
_ai = proxyHubAI(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
return _ai
}
if (binding) {
if (import.meta.dev) {
// Mock _ai to call NuxtHub Admin API to proxy CF account & API token
_ai = {
async run(model: string, params?: Record<string, unknown>) {
if (!hub.projectKey) {
throw createError({
statusCode: 500,
message: 'Missing hub.projectKey variable to use hubAI()'
})
}
if (!hub.userToken) {
throw createError({
statusCode: 500,
message: 'Missing hub.userToken variable to use hubAI()'
})
}
return $fetch(`/api/projects/${hub.projectKey}/ai/run`, {
baseURL: hub.url,
method: 'POST',
headers: {
authorization: `Bearer ${hub.userToken}`
},
body: { model, params },
responseType: params?.stream ? 'stream' : undefined
}).catch(handleProxyError)
}
} as Ai
} else {
_ai = binding as Ai
}
return _ai
RihanArfan marked this conversation as resolved.
Show resolved Hide resolved
}
return _ai
}
Expand All @@ -72,6 +109,7 @@ export function hubAI(): Ai {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const ai = proxyHubAI('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -82,14 +120,15 @@ export function hubAI(): Ai {
*
* @see https://developers.cloudflare.com/workers-ai/configuration/bindings/#methods
*/
export function proxyHubAI(projectUrl: string, secretKey?: string): Ai {
export function proxyHubAI(projectUrl: string, secretKey?: string, headers?: HeadersInit): Ai {
requireNuxtHubFeature('ai')

const aiAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/ai'),
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})
return {
Expand Down
9 changes: 6 additions & 3 deletions src/runtime/analytics/server/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ofetch } from 'ofetch'
import { joinURL } from 'ufo'
import { createError } from 'h3'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

const _datasets: Record<string, AnalyticsEngineDataset> = {}
Expand Down Expand Up @@ -45,7 +46,8 @@ export function hubAnalytics() {
const hub = useRuntimeConfig().hub
const binding = getAnalyticsBinding()
if (hub.remote && hub.projectUrl && !binding) {
return proxyHubAnalytics(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
return proxyHubAnalytics(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
}
const dataset = _useDataset()

Expand All @@ -57,13 +59,14 @@ export function hubAnalytics() {
}
}

export function proxyHubAnalytics(projectUrl: string, secretKey?: string) {
export function proxyHubAnalytics(projectUrl: string, secretKey?: string, headers?: HeadersInit) {
requireNuxtHubFeature('analytics')

const analyticsAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/analytics'),
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})

Expand Down
10 changes: 7 additions & 3 deletions src/runtime/blob/server/utils/blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { joinURL } from 'ufo'
import type { BlobType, FileSizeUnit, BlobUploadedPart, BlobListResult, BlobMultipartUpload, HandleMPUResponse, BlobMultipartOptions, BlobUploadOptions, BlobPutOptions, BlobEnsureOptions, BlobObject, BlobListOptions, BlobCredentialsOptions, BlobCredentials } from '@nuxthub/core'
import { streamToArrayBuffer } from '../../../utils/stream'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

const _r2_buckets: Record<string, R2Bucket> = {}
Expand Down Expand Up @@ -166,7 +167,8 @@ export function hubBlob(): HubBlob {
const hub = useRuntimeConfig().hub
const binding = getBlobBinding()
if (hub.remote && hub.projectUrl && !binding) {
return proxyHubBlob(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
return proxyHubBlob(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
}
const bucket = _useBucket()

Expand Down Expand Up @@ -350,6 +352,7 @@ export function hubBlob(): HubBlob {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const blob = proxyHubBlob('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -358,13 +361,14 @@ export function hubBlob(): HubBlob {
*
* @see https://hub.nuxt.com/docs/features/blob
*/
export function proxyHubBlob(projectUrl: string, secretKey?: string): HubBlob {
export function proxyHubBlob(projectUrl: string, secretKey?: string, headers?: HeadersInit): HubBlob {
requireNuxtHubFeature('blob')

const blobAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/blob'),
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})

Expand Down
6 changes: 4 additions & 2 deletions src/runtime/cache/server/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ export function hubCacheBinding(name: string = 'CACHE'): KVNamespace {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const cache = proxyHubCache('https://my-deployed-project.nuxt.dev', 'my-secret-key')
* const caches = await cache.list()
* ```
*
*/
export function proxyHubCache(projectUrl: string, secretKey?: string) {
export function proxyHubCache(projectUrl: string, secretKey?: string, headers?: HeadersInit) {
requireNuxtHubFeature('cache')

const cacheAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/cache'),
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})

Expand Down
10 changes: 7 additions & 3 deletions src/runtime/database/server/utils/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createError } from 'h3'
import type { H3Error } from 'h3'
import type { D1Database } from '@nuxthub/core'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

let _db: D1Database
Expand All @@ -28,7 +29,8 @@ export function hubDatabase(): D1Database {
// @ts-expect-error globalThis.__env__ is not defined
const binding = process.env.DB || globalThis.__env__?.DB || globalThis.DB
if (hub.remote && hub.projectUrl && !binding) {
_db = proxyHubDatabase(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
_db = proxyHubDatabase(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
return _db
}
if (binding) {
Expand All @@ -43,6 +45,7 @@ export function hubDatabase(): D1Database {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const db = proxyHubDatabase('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -51,14 +54,15 @@ export function hubDatabase(): D1Database {
*
* @see https://hub.nuxt.com/docs/features/database
*/
export function proxyHubDatabase(projectUrl: string, secretKey?: string): D1Database {
export function proxyHubDatabase(projectUrl: string, secretKey?: string, headers?: HeadersInit): D1Database {
requireNuxtHubFeature('database')

const d1API = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/database'),
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})
return {
Expand Down
10 changes: 7 additions & 3 deletions src/runtime/kv/server/utils/kv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { joinURL } from 'ufo'
import { createError } from 'h3'
import type { HubKV } from '@nuxthub/core'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

let _kv: HubKV
Expand All @@ -29,7 +30,8 @@ export function hubKV(): HubKV {
// @ts-expect-error globalThis.__env__ is not defined
const binding = process.env.KV || globalThis.__env__?.KV || globalThis.KV
if (hub.remote && hub.projectUrl && !binding) {
return proxyHubKV(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
return proxyHubKV(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
}
if (binding) {
const storage = createStorage({
Expand All @@ -48,6 +50,7 @@ export function hubKV(): HubKV {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const kv = proxyHubKV('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -56,14 +59,15 @@ export function hubKV(): HubKV {
*
* @see https://hub.nuxt.com/docs/features/kv
*/
export function proxyHubKV(projectUrl: string, secretKey?: string): HubKV {
export function proxyHubKV(projectUrl: string, secretKey?: string, headers?: Record<string, string>): HubKV {
requireNuxtHubFeature('kv')

const storage = createStorage({
driver: httpDriver({
base: joinURL(projectUrl, '/api/_hub/kv/'),
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})
})
Expand Down
10 changes: 10 additions & 0 deletions src/runtime/utils/cloudflareAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { HubConfig } from '~/src/features'

export function getCloudflareAccessHeaders(access: HubConfig['cloudflareAccess']): Record<string, string> {
const isCloudflareAccessEnabled = !!(access?.clientId && access?.clientSecret)
if (!isCloudflareAccessEnabled) return {}
return {
'CF-Access-Client-Id': access?.clientId,
'CF-Access-Client-Secret': access?.clientSecret
}
}
10 changes: 7 additions & 3 deletions src/runtime/vectorize/server/utils/vectorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { H3Error } from 'h3'
import type { RuntimeConfig } from 'nuxt/schema'
import type { Vectorize } from '../../../../types/vectorize'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

const _vectorize: Record<string, Vectorize> = {}
Expand Down Expand Up @@ -42,7 +43,8 @@ export function hubVectorize(index: VectorizeIndexes): Vectorize {
// @ts-expect-error globalThis.__env__ is not defined
const binding = process.env[bindingName] || globalThis.__env__?.[bindingName] || globalThis[bindingName]
if (hub.remote && hub.projectUrl && !binding) {
_vectorize[index] = proxyHubVectorize(index, hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
_vectorize[index] = proxyHubVectorize(index, hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
return _vectorize[index]
}
if (binding) {
Expand All @@ -58,6 +60,7 @@ export function hubVectorize(index: VectorizeIndexes): Vectorize {
* @param index The Vectorize index to access
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const db = proxyHubVectorize('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -70,14 +73,15 @@ export function hubVectorize(index: VectorizeIndexes): Vectorize {
*
* @see https://developers.cloudflare.com/vectorize/reference/client-api/
*/
export function proxyHubVectorize(index: VectorizeIndexes, projectUrl: string, secretKey?: string): Vectorize {
export function proxyHubVectorize(index: VectorizeIndexes, projectUrl: string, secretKey?: string, headers?: HeadersInit): Vectorize {
requireNuxtHubFeature('vectorize')

const vectorizeAPI = ofetch.create({
baseURL: joinURL(projectUrl, `/api/_hub/vectorize/${index}`),
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})
return {
Expand Down
16 changes: 16 additions & 0 deletions src/types/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,20 @@ export interface ModuleOptions {
[key: string]: string
}
}
/**
* Cloudflare Access authentication for remote storage.
* @see https://hub.nuxt.com/recipes/cloudflare-access
*/
cloudflareAccess?: {
/**
* The client ID for Cloudflare Access.
* @default process.env.NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID
*/
clientId?: string
/**
* The client secret for Cloudflare Access.
* @default process.env.NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET
*/
clientSecret?: string
}
}