Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,27 @@ jobs:
- name: Install JS deps
run: pnpm install --frozen-lockfile

- name: Resolve API + gateway URLs
# The mobile shell defaults apiFetch to /api which only works when
# the web bundle is hosted on the same origin as the API. Capacitor
# serves from https://localhost, so without baking VITE_API_BASE
# into the build, every login attempt 404s and `start.challenge`
# blows up. Allow the build to fall back to the values committed
# in apps/mobile/.env.production if the repo vars are not set.
run: |
: "${VITE_API_BASE:=${{ vars.VITE_API_BASE }}}"
: "${VITE_GATEWAY_URL:=${{ vars.VITE_GATEWAY_URL }}}"
if [ -z "$VITE_API_BASE" ] && [ -f apps/mobile/.env.production ]; then
set -a; . apps/mobile/.env.production; set +a
fi
echo "VITE_API_BASE=$VITE_API_BASE" >> "$GITHUB_ENV"
echo "VITE_GATEWAY_URL=$VITE_GATEWAY_URL" >> "$GITHUB_ENV"
echo "Building web bundle against API=$VITE_API_BASE GW=$VITE_GATEWAY_URL"

- name: Build web bundle
env:
VITE_API_BASE: ${{ env.VITE_API_BASE }}
VITE_GATEWAY_URL: ${{ env.VITE_GATEWAY_URL }}
run: pnpm --filter tempest-web build

- name: Add Android platform
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,21 @@ jobs:
gem install cocoapods --no-document
pod --version

- name: Resolve API + gateway URLs
run: |
: "${VITE_API_BASE:=${{ vars.VITE_API_BASE }}}"
: "${VITE_GATEWAY_URL:=${{ vars.VITE_GATEWAY_URL }}}"
if [ -z "$VITE_API_BASE" ] && [ -f apps/mobile/.env.production ]; then
set -a; . apps/mobile/.env.production; set +a
fi
echo "VITE_API_BASE=$VITE_API_BASE" >> "$GITHUB_ENV"
echo "VITE_GATEWAY_URL=$VITE_GATEWAY_URL" >> "$GITHUB_ENV"
echo "Building web bundle against API=$VITE_API_BASE GW=$VITE_GATEWAY_URL"

- name: Build web bundle
env:
VITE_API_BASE: ${{ env.VITE_API_BASE }}
VITE_GATEWAY_URL: ${{ env.VITE_GATEWAY_URL }}
run: pnpm --filter tempest-web build

- name: Add iOS platform
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Default API + Gateway URLs the mobile build embeds when the workflow has
# no repo vars set. Override per-deploy by setting VITE_API_BASE /
# VITE_GATEWAY_URL as repository variables in GitHub
# (Settings -> Secrets and variables -> Actions -> Variables), or by
# editing this file before tagging a release.
#
# Example (replace with the URLs printed by `railway up`):
# VITE_API_BASE=https://tempest-api-production-2899.up.railway.app
# VITE_GATEWAY_URL=wss://tempest-gateway-production-1213ce.up.railway.app/gateway

VITE_API_BASE=https://tempest-api-production-2899.up.railway.app
VITE_GATEWAY_URL=wss://tempest-gateway-production-1213ce.up.railway.app/gateway
14 changes: 13 additions & 1 deletion apps/web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,24 @@ export async function apiFetch<T = unknown>(

const text = await res.text();
let data: any = null;
let parseFailed = false;
if (text.length > 0) {
try { data = JSON.parse(text); } catch { /* ignore */ }
try { data = JSON.parse(text); } catch { parseFailed = true; }
}
if (!res.ok) {
throw new TempestError(res.status, data ?? { code: "http_error", message: res.statusText });
}
// If we got a 2xx but the body is empty or non-JSON, that almost always
// means the request was rewritten by a 404 catch-all (e.g. the mobile
// shell's webview hit its own host instead of the API). Throwing here
// gives the caller a real error instead of a null reference crash later.
if (data === null) {
const sample = text.slice(0, 80).replace(/\s+/g, " ");
const msg = parseFailed
? `expected JSON from ${url}, got: ${sample}`
: `empty response from ${url}`;
throw new TempestError(res.status, { code: "bad_response", message: msg });
}
return data as T;
}

Expand Down
17 changes: 16 additions & 1 deletion crates/tempest-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,26 @@ async fn main() -> anyhow::Result<()> {
.merge(routes::uploads::private_router())
.layer(axum_mw::from_fn_with_state(state.clone(), middleware::auth::require_auth));

let allow_origins: Vec<HeaderValue> = cfg
// The public web origin lives in cfg.allowed_origins. Capacitor wraps
// the same web bundle into Android (https://localhost) and iOS
// (capacitor://localhost) shells, so we always allow those two so the
// mobile builds work without per-deploy origin config.
let mut allow_origins: Vec<HeaderValue> = cfg
.allowed_origins
.iter()
.filter_map(|o| HeaderValue::from_str(o).ok())
.collect();
for fixed in [
"https://localhost",
"http://localhost",
"capacitor://localhost",
"ionic://localhost",
"tauri://localhost",
] {
if let Ok(v) = HeaderValue::from_str(fixed) {
allow_origins.push(v);
}
}
// CORS spec: when allow_credentials is true the headers and methods must
// be explicit lists, not wildcards.
let cors = CorsLayer::new()
Expand Down
Loading