From fc30ab69568ee3fb29ab388534af5d99534e2fbb Mon Sep 17 00:00:00 2001 From: Connor Lewis <50084106+imconnorngl@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:50:42 +0100 Subject: [PATCH 1/4] feat: bind SERVER_API_TOKEN from Secrets Store in wrangler config Add secrets_store_secrets bindings for the production and development environments so the Worker receives the server API key from Cloudflare Secrets Store instead of a plain Worker secret. --- wrangler.jsonc | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 6e6f49c6f..3939b1eaf 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -47,7 +47,14 @@ "vars": { "PUBLIC_SERVER_API_URL": "https://sky.shiiyu.moe/api/", "PUBLIC_API_URL": "https://sky.shiiyu.moe/api/" - } + }, + "secrets_store_secrets": [ + { + "binding": "SERVER_API_TOKEN", + "store_id": "1a9462229b034bc78858f53bc58fb477", + "secret_name": "skycrypt-server-api-key" + } + ] }, "development": { "name": "skycrypt-embed", @@ -60,7 +67,14 @@ "vars": { "PUBLIC_SERVER_API_URL": "https://sky.shiiyu.moe/api/", "PUBLIC_API_URL": "https://sky.shiiyu.moe/api/" - } + }, + "secrets_store_secrets": [ + { + "binding": "SERVER_API_TOKEN", + "store_id": "739b83a68daf486ea3ec077b812048b4", + "secret_name": "skycrypt-server-api-key" + } + ] } } } From 5b3fd31fe54b00de6bd0e604bdb9334d425b6977 Mon Sep 17 00:00:00 2001 From: Connor Lewis <50084106+imconnorngl@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:09:57 +0100 Subject: [PATCH 2/4] fix: resolve SERVER_API_TOKEN from Secrets Store binding Secrets Store bindings expose the value via an async get() rather than a plain string, so the header was going to be sent as [object Object]. Resolve the token at request time, calling get() when the binding is present and falling back to the plain string from .env for local dev. The resolved value is cached per isolate; failed lookups are not cached. --- src/lib/shared/api/mutator/custom-instance.ts | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/lib/shared/api/mutator/custom-instance.ts b/src/lib/shared/api/mutator/custom-instance.ts index 1ce916c86..bfddcd90e 100644 --- a/src/lib/shared/api/mutator/custom-instance.ts +++ b/src/lib/shared/api/mutator/custom-instance.ts @@ -3,7 +3,35 @@ import { env as envPrivate } from "$env/dynamic/private"; import { env as envPublic } from "$env/dynamic/public"; const { PUBLIC_SERVER_API_URL } = envPublic; -const { SERVER_API_TOKEN } = envPrivate; + +// Cloudflare Secrets Store bindings expose the value through an async `get()` +// rather than as a plain string. +type SecretsStoreBinding = { get(): Promise }; + +let cachedServerApiToken: Promise | undefined; + +// NOTE: Resolve the server API token from either a Secrets Store binding +// (production/development Workers) or a plain string (local `.env`). +const getServerApiToken = (): Promise => { + if (cachedServerApiToken) return cachedServerApiToken; + + const token: unknown = envPrivate.SERVER_API_TOKEN; + let resolved: Promise; + + if (token && typeof token === "object" && typeof (token as SecretsStoreBinding).get === "function") { + resolved = (token as SecretsStoreBinding).get(); + } else { + resolved = Promise.resolve(typeof token === "string" ? token : ""); + } + + cachedServerApiToken = resolved.catch((error) => { + // Don't cache a failed lookup so the next request retries. + cachedServerApiToken = undefined; + throw error; + }); + + return cachedServerApiToken; +}; // NOTE: Supports cases where `content-type` is other than `json` const getBody = (c: Response | Request): Promise => { @@ -26,7 +54,9 @@ const getUrl = (contextUrl: string): string => { }; // NOTE: Add headers -const getHeaders = (headers?: HeadersInit): HeadersInit => { +const getHeaders = async (headers?: HeadersInit): Promise => { + const serverApiToken = await getServerApiToken(); + try { const { request } = getRequestEvent(); @@ -39,14 +69,14 @@ const getHeaders = (headers?: HeadersInit): HeadersInit => { const mergedHeaders: HeadersInit = { ...requestHeadersObj, ...headers, - "X-API-Token": SERVER_API_TOKEN + "X-API-Token": serverApiToken }; return mergedHeaders; } catch { return { ...headers, - "X-API-Token": SERVER_API_TOKEN + "X-API-Token": serverApiToken }; } }; @@ -61,7 +91,7 @@ export const customFetch = async (url: string, options: RequestInit): Promise } const requestUrl = getUrl(url); - const requestHeaders = getHeaders(options.headers); + const requestHeaders = await getHeaders(options.headers); const requestInit: RequestInit = { ...options, From f528a8ab4e8d37915d66b4a0a25196762e7b020b Mon Sep 17 00:00:00 2001 From: Connor Lewis <50084106+imconnorngl@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:15:54 +0100 Subject: [PATCH 3/4] refactor: read SERVER_API_TOKEN straight from the Worker binding Use event.platform.env.SERVER_API_TOKEN.get() directly instead of sniffing the shape of the dynamic env value and caching it. Falls back to the .env string when the binding is absent (local vite dev). Adds a minimal App.Platform type for the binding. --- src/app.d.ts | 7 +- src/lib/shared/api/mutator/custom-instance.ts | 76 ++++--------------- 2 files changed, 19 insertions(+), 64 deletions(-) diff --git a/src/app.d.ts b/src/app.d.ts index 031435aaa..db9b8962d 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -9,7 +9,12 @@ declare global { // interface Error {} // interface PageData {} // interface PageState {} - // interface Platform {} + interface Platform { + env: { + /** Cloudflare Secrets Store binding. Absent in local dev, where `.env` is used instead. */ + SERVER_API_TOKEN?: { get(): Promise }; + }; + } namespace Superforms { type Message = { type: "error" | "success"; text: string }; } diff --git a/src/lib/shared/api/mutator/custom-instance.ts b/src/lib/shared/api/mutator/custom-instance.ts index bfddcd90e..5bcf16706 100644 --- a/src/lib/shared/api/mutator/custom-instance.ts +++ b/src/lib/shared/api/mutator/custom-instance.ts @@ -4,35 +4,6 @@ import { env as envPublic } from "$env/dynamic/public"; const { PUBLIC_SERVER_API_URL } = envPublic; -// Cloudflare Secrets Store bindings expose the value through an async `get()` -// rather than as a plain string. -type SecretsStoreBinding = { get(): Promise }; - -let cachedServerApiToken: Promise | undefined; - -// NOTE: Resolve the server API token from either a Secrets Store binding -// (production/development Workers) or a plain string (local `.env`). -const getServerApiToken = (): Promise => { - if (cachedServerApiToken) return cachedServerApiToken; - - const token: unknown = envPrivate.SERVER_API_TOKEN; - let resolved: Promise; - - if (token && typeof token === "object" && typeof (token as SecretsStoreBinding).get === "function") { - resolved = (token as SecretsStoreBinding).get(); - } else { - resolved = Promise.resolve(typeof token === "string" ? token : ""); - } - - cachedServerApiToken = resolved.catch((error) => { - // Don't cache a failed lookup so the next request retries. - cachedServerApiToken = undefined; - throw error; - }); - - return cachedServerApiToken; -}; - // NOTE: Supports cases where `content-type` is other than `json` const getBody = (c: Response | Request): Promise => { const contentType = c.headers.get("content-type"); @@ -53,52 +24,31 @@ const getUrl = (contextUrl: string): string => { return requestUrl.toString(); }; -// NOTE: Add headers -const getHeaders = async (headers?: HeadersInit): Promise => { - const serverApiToken = await getServerApiToken(); - +// NOTE: Returns null when called outside of a request (e.g. at build time) +const tryGetRequestEvent = () => { try { - const { request } = getRequestEvent(); - - // Convert the request Headers object to a plain object - const requestHeadersObj: Record = {}; - request.headers.forEach((value, key) => { - requestHeadersObj[key] = value; - }); - - const mergedHeaders: HeadersInit = { - ...requestHeadersObj, - ...headers, - "X-API-Token": serverApiToken - }; - - return mergedHeaders; + return getRequestEvent(); } catch { - return { - ...headers, - "X-API-Token": serverApiToken - }; + return null; } }; export const customFetch = async (url: string, options: RequestInit): Promise => { - let fetchFunction = fetch; - try { - const event = getRequestEvent(); - fetchFunction = event.fetch; - } catch { - // Ignore this, we just won't have access to the request for this call - } + const event = tryGetRequestEvent(); - const requestUrl = getUrl(url); - const requestHeaders = await getHeaders(options.headers); + // Deployed Workers read the token from the Secrets Store binding; local dev falls back to `.env` + const serverApiToken = (await event?.platform?.env.SERVER_API_TOKEN?.get()) ?? envPrivate.SERVER_API_TOKEN; const requestInit: RequestInit = { ...options, - headers: requestHeaders + headers: { + ...(event ? Object.fromEntries(event.request.headers) : {}), + ...options.headers, + "X-API-Token": serverApiToken + } }; - const response = await fetchFunction(requestUrl, requestInit); + const response = await (event?.fetch ?? fetch)(getUrl(url), requestInit); const data = await getBody(response); return { status: response.status, data, headers: response.headers } as T; From acde069d6d83eef616e3731d2c9bf650bd9b8deb Mon Sep 17 00:00:00 2001 From: Connor Lewis <50084106+imconnorngl@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:21:27 +0100 Subject: [PATCH 4/4] fix: accept a plain-string SERVER_API_TOKEN during build and local dev The platform proxy used by vite build/dev exposes SERVER_API_TOKEN as a string from the process env, so calling get() on it broke prerendering in CI. Only call get() when the value is the Secrets Store binding. --- src/app.d.ts | 4 ++-- src/lib/shared/api/mutator/custom-instance.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app.d.ts b/src/app.d.ts index db9b8962d..651f4f7c7 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -11,8 +11,8 @@ declare global { // interface PageState {} interface Platform { env: { - /** Cloudflare Secrets Store binding. Absent in local dev, where `.env` is used instead. */ - SERVER_API_TOKEN?: { get(): Promise }; + /** Secrets Store binding in deployed Workers; a plain string in local dev and prerender. */ + SERVER_API_TOKEN?: string | { get(): Promise }; }; } namespace Superforms { diff --git a/src/lib/shared/api/mutator/custom-instance.ts b/src/lib/shared/api/mutator/custom-instance.ts index 5bcf16706..a3e76ed6e 100644 --- a/src/lib/shared/api/mutator/custom-instance.ts +++ b/src/lib/shared/api/mutator/custom-instance.ts @@ -36,8 +36,9 @@ const tryGetRequestEvent = () => { export const customFetch = async (url: string, options: RequestInit): Promise => { const event = tryGetRequestEvent(); - // Deployed Workers read the token from the Secrets Store binding; local dev falls back to `.env` - const serverApiToken = (await event?.platform?.env.SERVER_API_TOKEN?.get()) ?? envPrivate.SERVER_API_TOKEN; + // Deployed Workers get a Secrets Store binding; local dev and prerender get a plain string + const token = event?.platform?.env.SERVER_API_TOKEN ?? envPrivate.SERVER_API_TOKEN; + const serverApiToken = typeof token === "string" ? token : await token.get(); const requestInit: RequestInit = { ...options,