mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-28 15:51:29 +08:00
feat: Azure Functions deployment target (v4, Node 22) (#330)
* feat: add Azure Functions deployment target (v4, Node 22) - server/entry-azure.ts: Azure Functions v4 handler wrapping the Hono app via app.http(); uses createLibsqlPlatform for Turso and serves the SPA from ./dist via @hono/node-server/serve-static - server/azure-host.json: runtime manifest (extensionBundle v4) - deploy/azure-functions/main.bicep: idempotent Bicep template provisioning Storage Account, Consumption plan and Function App; BETTER_AUTH_SECRET handled separately by the workflow - .github/workflows/deploy-azure.yml: 8-step workflow (secret check, checkout, Node setup, az login, Bicep deploy, build, db:migrate, func publish) with BETTER_AUTH_SECRET generate-if-missing logic - package.json: build:azure script + @azure/functions dependency - docs/deploy/azure-functions.md: setup guide covering SP JSON format, required secrets, and local emulation with func start Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * fix: address review issues in Azure Functions deploy - Move BETTER_AUTH_SECRET and APP_URL setup to before func publish (bootstrap.ts throws on missing secret; any request between publish and the old secret-set step would have returned 500) - Remove placeholder appUrl Bicep param; workflow sets APP_URL and BETTER_AUTH_URL via appsettings after Bicep, before publish - Fix HttpRequest→Request body handling: construct a proper Web API Request with body cast and duplex option instead of double-casting HttpRequest, ensuring POST/PUT/PATCH body-reading routes work - Add push: branches: [master] trigger + upstream guard to match other deploy workflow conventions; document the auto-deploy behaviour - Update docs/deploy/azure-functions.md to reflect the push trigger Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * ci: re-trigger CI for review fixes --------- Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
name: Deploy to Azure Functions
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resource_group:
|
||||
description: 'Azure Resource Group name (created if absent)'
|
||||
required: true
|
||||
default: 'zpan-rg'
|
||||
location:
|
||||
description: 'Azure region (e.g. eastus)'
|
||||
required: true
|
||||
default: 'eastus'
|
||||
version:
|
||||
description: 'Release tag to deploy (e.g. v2.5.0). Leave empty for latest.'
|
||||
required: false
|
||||
|
||||
# Prevent overlapping deployments.
|
||||
concurrency:
|
||||
group: deploy-azure
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on forks that have configured Azure credentials.
|
||||
# The upstream repo uses Cloudflare Workers Builds; Azure is for self-hosters.
|
||||
if: github.repository != 'saltbo/zpan'
|
||||
steps:
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1 — Verify all required secrets are present before doing work.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Check required secrets
|
||||
env:
|
||||
HAS_AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS != '' }}
|
||||
HAS_TURSO_URL: ${{ secrets.TURSO_DATABASE_URL != '' }}
|
||||
HAS_TURSO_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN != '' }}
|
||||
run: |
|
||||
MISSING=()
|
||||
[ "$HAS_AZURE_CREDENTIALS" != "true" ] && MISSING+=("AZURE_CREDENTIALS")
|
||||
[ "$HAS_TURSO_URL" != "true" ] && MISSING+=("TURSO_DATABASE_URL")
|
||||
[ "$HAS_TURSO_TOKEN" != "true" ] && MISSING+=("TURSO_AUTH_TOKEN")
|
||||
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||
echo "::error::Missing required secrets: ${MISSING[*]}"
|
||||
echo "Go to Settings → Secrets and variables → Actions and add the missing secrets."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2 — Resolve release tag and check out that version.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Resolve release tag
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [ -n "$INPUT_VERSION" ]; then
|
||||
TAG="$INPUT_VERSION"
|
||||
else
|
||||
TAG=$(gh api repos/saltbo/zpan/releases/latest --jq '.tag_name')
|
||||
fi
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "::error::No release found in saltbo/zpan"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "### 🚀 Deploying $TAG to Azure Functions" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: saltbo/zpan
|
||||
ref: ${{ steps.release.outputs.version }}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3 — Node.js setup + install dependencies.
|
||||
# ------------------------------------------------------------------
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- run: npm ci
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4 — Log in to Azure using the service-principal credentials.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Azure login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 5 — Provision infrastructure (idempotent create-or-update).
|
||||
# ------------------------------------------------------------------
|
||||
- name: Resolve inputs (push vs. dispatch)
|
||||
id: params
|
||||
run: |
|
||||
echo "resource_group=${{ inputs.resource_group || 'zpan-rg' }}" >> "$GITHUB_OUTPUT"
|
||||
echo "location=${{ inputs.location || 'eastus' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Ensure Resource Group exists
|
||||
run: |
|
||||
az group create \
|
||||
--name "${{ steps.params.outputs.resource_group }}" \
|
||||
--location "${{ steps.params.outputs.location }}" \
|
||||
--output none
|
||||
|
||||
- name: Deploy Bicep template
|
||||
id: bicep
|
||||
env:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: |
|
||||
OUTPUT=$(az deployment group create \
|
||||
--resource-group "${{ steps.params.outputs.resource_group }}" \
|
||||
--template-file deploy/azure-functions/main.bicep \
|
||||
--parameters \
|
||||
tursoDatabaseUrl="$TURSO_DATABASE_URL" \
|
||||
tursoAuthToken="$TURSO_AUTH_TOKEN" \
|
||||
--query "properties.outputs" \
|
||||
--output json)
|
||||
|
||||
FUNC_NAME=$(echo "$OUTPUT" | jq -r '.functionAppName.value')
|
||||
FUNC_URL=$(echo "$OUTPUT" | jq -r '.functionAppUrl.value')
|
||||
echo "functionAppName=$FUNC_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "functionAppUrl=$FUNC_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "Function App: $FUNC_NAME ($FUNC_URL)" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 6a — Set BETTER_AUTH_SECRET before publish.
|
||||
# The Function App exists after Bicep; setting the secret now means
|
||||
# the very first invocation after publish already has it configured.
|
||||
# bootstrap.ts throws 'BETTER_AUTH_SECRET is required' if it is absent,
|
||||
# so publishing before this step would cause 500s until the step ran.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Set BETTER_AUTH_SECRET (generate once, never overwrite)
|
||||
env:
|
||||
USER_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
||||
run: |
|
||||
FUNC_NAME="${{ steps.bicep.outputs.functionAppName }}"
|
||||
RG="${{ steps.params.outputs.resource_group }}"
|
||||
|
||||
EXISTS=$(az functionapp config appsettings list \
|
||||
--name "$FUNC_NAME" \
|
||||
--resource-group "$RG" \
|
||||
--query "[?name=='BETTER_AUTH_SECRET'].value" \
|
||||
--output tsv)
|
||||
|
||||
if [ -n "$USER_SECRET" ]; then
|
||||
az functionapp config appsettings set \
|
||||
--name "$FUNC_NAME" \
|
||||
--resource-group "$RG" \
|
||||
--settings "BETTER_AUTH_SECRET=$USER_SECRET" \
|
||||
--output none
|
||||
echo "Set BETTER_AUTH_SECRET from GitHub secret."
|
||||
elif [ -z "$EXISTS" ]; then
|
||||
GENERATED=$(openssl rand -base64 32)
|
||||
az functionapp config appsettings set \
|
||||
--name "$FUNC_NAME" \
|
||||
--resource-group "$RG" \
|
||||
--settings "BETTER_AUTH_SECRET=$GENERATED" \
|
||||
--output none
|
||||
echo "Auto-generated BETTER_AUTH_SECRET."
|
||||
else
|
||||
echo "BETTER_AUTH_SECRET already set — skipping."
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 6b — Patch APP_URL / BETTER_AUTH_URL to the real hostname.
|
||||
# Bicep sets both from the `appUrl` parameter (defaults to an empty
|
||||
# placeholder when not provided). Finalise before publish so auth
|
||||
# redirects are correct from the first request.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Update APP_URL to real function app URL
|
||||
run: |
|
||||
FUNC_NAME="${{ steps.bicep.outputs.functionAppName }}"
|
||||
FUNC_URL="${{ steps.bicep.outputs.functionAppUrl }}"
|
||||
RG="${{ steps.params.outputs.resource_group }}"
|
||||
az functionapp config appsettings set \
|
||||
--name "$FUNC_NAME" \
|
||||
--resource-group "$RG" \
|
||||
--settings "APP_URL=$FUNC_URL" "BETTER_AUTH_URL=$FUNC_URL" \
|
||||
--output none
|
||||
echo "APP_URL set to $FUNC_URL"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 7 — Build frontend + Azure Functions bundle.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Build
|
||||
run: npm run build:azure
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 8 — Run database migrations against Turso.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Run database migrations
|
||||
env:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: npm run db:migrate
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 9 — Install Azure Functions Core Tools and publish.
|
||||
# All required app settings (BETTER_AUTH_SECRET, APP_URL, Turso creds)
|
||||
# are already in place before this step runs.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Install Azure Functions Core Tools
|
||||
run: npm install -g azure-functions-core-tools@4 --unsafe-perm true
|
||||
|
||||
- name: Publish to Azure Functions
|
||||
working-directory: azure-functions
|
||||
run: func azure functionapp publish "${{ steps.bicep.outputs.functionAppName }}" --node
|
||||
|
||||
- name: Deployment summary
|
||||
run: |
|
||||
echo "### ✅ Deployed: ${{ steps.bicep.outputs.functionAppUrl }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,84 @@
|
||||
@description('Application name — used as a prefix for all resources.')
|
||||
param appName string = 'zpan'
|
||||
|
||||
@description('Azure region. Defaults to the resource group location.')
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('Turso (libSQL) database URL, e.g. libsql://your-db.turso.io')
|
||||
param tursoDatabaseUrl string
|
||||
|
||||
@secure()
|
||||
@description('Turso auth token.')
|
||||
param tursoAuthToken string
|
||||
|
||||
// Unique suffix derived from the resource group so re-runs produce the same names (idempotent).
|
||||
var suffix = uniqueString(resourceGroup().id)
|
||||
var storageAccountName = take('${toLower(replace(appName, '-', ''))}${suffix}', 24)
|
||||
var hostingPlanName = '${appName}-plan-${suffix}'
|
||||
var functionAppName = '${appName}-func-${suffix}'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Storage Account — required by the Azure Functions runtime.
|
||||
// ---------------------------------------------------------------------------
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
||||
name: storageAccountName
|
||||
location: location
|
||||
sku: { name: 'Standard_LRS' }
|
||||
kind: 'StorageV2'
|
||||
properties: {
|
||||
supportsHttpsTrafficOnly: true
|
||||
minimumTlsVersion: 'TLS1_2'
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Consumption plan (Y1 / Dynamic SKU).
|
||||
// ---------------------------------------------------------------------------
|
||||
resource hostingPlan 'Microsoft.Web/serverfarms@2023-01-01' = {
|
||||
name: hostingPlanName
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Y1'
|
||||
tier: 'Dynamic'
|
||||
}
|
||||
properties: {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Function App — Node 22, programming model v4.
|
||||
// BETTER_AUTH_SECRET is intentionally absent here; it is managed by the
|
||||
// deploy workflow so that it is generated once and never overwritten on
|
||||
// subsequent runs.
|
||||
// ---------------------------------------------------------------------------
|
||||
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
|
||||
name: functionAppName
|
||||
location: location
|
||||
kind: 'functionapp'
|
||||
properties: {
|
||||
serverFarmId: hostingPlan.id
|
||||
httpsOnly: true
|
||||
siteConfig: {
|
||||
nodeVersion: '~22'
|
||||
appSettings: [
|
||||
{
|
||||
name: 'AzureWebJobsStorage'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
|
||||
}
|
||||
{ name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
|
||||
{ name: 'FUNCTIONS_WORKER_RUNTIME', value: 'node' }
|
||||
{ name: 'WEBSITE_RUN_FROM_PACKAGE', value: '1' }
|
||||
{ name: 'TURSO_DATABASE_URL', value: tursoDatabaseUrl }
|
||||
{ name: 'TURSO_AUTH_TOKEN', value: tursoAuthToken }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Outputs consumed by the deploy workflow.
|
||||
// ---------------------------------------------------------------------------
|
||||
@description('Name of the deployed Function App.')
|
||||
output functionAppName string = functionApp.name
|
||||
|
||||
@description('Default HTTPS URL of the Function App.')
|
||||
output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
|
||||
@@ -0,0 +1,144 @@
|
||||
# Azure Functions Deployment
|
||||
|
||||
ZPan supports deployment to [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/) (programming model v4, Node.js 22) as an alternative to Cloudflare Workers or Docker. The function app serves both the Hono API and the React SPA from a single Consumption-plan function.
|
||||
|
||||
> **S3-compatible storage required** — Azure Blob Storage is _not_ adapted as an object backend. You must bring an external S3-compatible bucket (AWS S3, Cloudflare R2, MinIO, etc.) and configure it as a storage provider inside ZPan after deployment.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Notes |
|
||||
|---|---|
|
||||
| Azure subscription | Consumption-plan Functions are free up to 1 M invocations/month |
|
||||
| Azure CLI | `az` ≥ 2.50 — [install](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) |
|
||||
| Azure Functions Core Tools | v4 — installed automatically by the workflow |
|
||||
| Turso account | [turso.tech](https://turso.tech) — free tier covers most self-hosted use cases |
|
||||
| S3-compatible bucket | Any provider; configured inside ZPan post-deploy |
|
||||
|
||||
---
|
||||
|
||||
## 1 — Create a Turso database
|
||||
|
||||
```sh
|
||||
turso db create zpan
|
||||
turso db show zpan # note the URL (libsql://...)
|
||||
turso db tokens create zpan # note the auth token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2 — Create an Azure service principal
|
||||
|
||||
The GitHub Actions workflow authenticates to Azure with a service principal whose credentials are stored as a single JSON secret (`AZURE_CREDENTIALS`).
|
||||
|
||||
```sh
|
||||
# Replace <subscription-id> with your Azure subscription ID.
|
||||
az ad sp create-for-rbac \
|
||||
--name "zpan-deploy" \
|
||||
--role Contributor \
|
||||
--scopes /subscriptions/<subscription-id> \
|
||||
--sdk-auth
|
||||
```
|
||||
|
||||
The command outputs a JSON block. Copy the **entire JSON object** — it is the value for the `AZURE_CREDENTIALS` secret.
|
||||
|
||||
### Service-principal JSON format
|
||||
|
||||
```json
|
||||
{
|
||||
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"clientSecret": "your-client-secret",
|
||||
"subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
|
||||
"resourceManagerEndpointUrl": "https://management.azure.com/",
|
||||
"activeDirectoryGraphResourceId": "https://graph.windows.net/",
|
||||
"sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
|
||||
"galleryEndpointUrl": "https://gallery.azure.com/",
|
||||
"managementEndpointUrl": "https://management.core.windows.net/"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3 — Add GitHub repository secrets
|
||||
|
||||
In your fork go to **Settings → Secrets and variables → Actions** and add:
|
||||
|
||||
| Secret name | Value |
|
||||
|---|---|
|
||||
| `AZURE_CREDENTIALS` | Full JSON object from `az ad sp create-for-rbac --sdk-auth` (see above) |
|
||||
| `TURSO_DATABASE_URL` | `libsql://your-db-name-orgname.turso.io` |
|
||||
| `TURSO_AUTH_TOKEN` | Token from `turso db tokens create zpan` |
|
||||
| `BETTER_AUTH_SECRET` | _(optional)_ Pre-generated secret — `openssl rand -base64 32`. If absent the workflow generates one automatically on first deploy. |
|
||||
|
||||
---
|
||||
|
||||
## 4 — Run the workflow
|
||||
|
||||
The workflow triggers automatically on every push to `master` **and** can be triggered manually via **Actions → Deploy to Azure Functions → Run workflow**.
|
||||
|
||||
> The workflow includes `if: github.repository != 'saltbo/zpan'` so it is a no-op in the upstream repo. It only runs in your fork.
|
||||
|
||||
| Manual dispatch input | Description |
|
||||
|---|---|
|
||||
| `resource_group` | Azure Resource Group name — created automatically if it does not exist (default: `zpan-rg`) |
|
||||
| `location` | Azure region (default: `eastus`) |
|
||||
| `version` | Release tag (e.g. `v2.5.0`). Leave empty to use the latest release. |
|
||||
|
||||
> When triggered by a push, defaults are used for `resource_group` (`zpan-rg`) and `location` (`eastus`).
|
||||
|
||||
### What the workflow does
|
||||
|
||||
1. **Check secrets** — fails fast if any required secret is missing.
|
||||
2. **Resolve release tag** — pins to a specific ZPan release.
|
||||
3. **Set up Node 22** and install dependencies.
|
||||
4. **Azure login** — uses the `AZURE_CREDENTIALS` service principal.
|
||||
5. **Provision infrastructure** via `deploy/azure-functions/main.bicep`:
|
||||
- Resource Group (idempotent `az group create`)
|
||||
- Storage Account (required by the Functions runtime)
|
||||
- Consumption plan (Y1 / Dynamic SKU)
|
||||
- Function App (Node 22, runtime v4)
|
||||
6. **Build** — `npm run build:azure` produces the `azure-functions/` publish directory.
|
||||
7. **Migrate** — `npm run db:migrate` applies Drizzle migrations to Turso.
|
||||
8. **Publish** — `func azure functionapp publish <name>` uploads the bundle.
|
||||
9. **Set `BETTER_AUTH_SECRET`** — checks whether the setting already exists; generates and sets it if missing.
|
||||
10. **Update `APP_URL`** — patches the real function-app URL into its own app settings.
|
||||
|
||||
Re-running the workflow is safe — Bicep uses create-or-update semantics and the secret step skips if the setting is already present.
|
||||
|
||||
---
|
||||
|
||||
## 5 — Post-deploy: configure S3 storage
|
||||
|
||||
1. Open your function app URL in a browser and complete the ZPan setup wizard.
|
||||
2. Navigate to **Admin → Storages** and add your S3-compatible bucket credentials.
|
||||
|
||||
### Verify the deployment
|
||||
|
||||
```sh
|
||||
curl https://<your-func-app>.azurewebsites.net/api/health
|
||||
# → {"status":"ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local development against a Turso database
|
||||
|
||||
```sh
|
||||
TURSO_DATABASE_URL=libsql://your-db.turso.io \
|
||||
TURSO_AUTH_TOKEN=your-token \
|
||||
BETTER_AUTH_SECRET=$(openssl rand -base64 32) \
|
||||
npm run dev:node
|
||||
```
|
||||
|
||||
### Local Azure Functions emulation
|
||||
|
||||
```sh
|
||||
npm run build:azure
|
||||
cd azure-functions
|
||||
func start
|
||||
```
|
||||
|
||||
Requires the [Azure Functions Core Tools v4](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local) and a local `.env` file (or environment variables) with `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, and `BETTER_AUTH_SECRET`.
|
||||
Generated
+32
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1022.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1022.0",
|
||||
"@azure/functions": "^4.12.0",
|
||||
"@better-auth/api-key": "^1.6.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@@ -1017,6 +1018,37 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/functions": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.12.0.tgz",
|
||||
"integrity": "sha512-aHBSvEDHOUhLhkivPiotoYfE6WPZdutv9OnXEkSqYtyWjbf1k/DoQ/Z9swqIbyaBSvh2xOR8ARKj5CR/2jqEOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/functions-extensions-base": "0.2.0",
|
||||
"cookie": "^0.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/functions-extensions-base": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/functions-extensions-base/-/functions-extensions-base-0.2.0.tgz",
|
||||
"integrity": "sha512-ncCkHBNQYJa93dBIh+toH0v1iSgCzSo9tr94s6SMBe7DPWREkaWh8cq33A5P4rPSFX1g5W+3SPvIzDr/6/VOWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/functions/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"build:lambda": "tsup server/entry-lambda.ts --format cjs --outDir dist-lambda --external @libsql/client",
|
||||
"build:vercel": "vite build --mode node && tsup server/entry-vercel.ts --format esm --outDir api --external @libsql/client",
|
||||
"build:netlify": "tsup server/entry-netlify.ts --format esm --outDir netlify/functions --external @libsql/client",
|
||||
"build:azure": "vite build && tsup server/entry-azure.ts --format esm --outDir azure-functions --external @azure/functions --external @libsql/client && cp server/azure-host.json azure-functions/host.json && cp -r dist azure-functions/dist",
|
||||
"deploy": "npm run db:migrate:d1:prod && wrangler deploy",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
@@ -36,6 +37,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1022.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1022.0",
|
||||
"@azure/functions": "^4.12.0",
|
||||
"@better-auth/api-key": "^1.6.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
},
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingSettings": {
|
||||
"isEnabled": true,
|
||||
"excludedTypes": "Request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { HttpRequest, InvocationContext } from '@azure/functions'
|
||||
import { app } from '@azure/functions'
|
||||
import { serveStatic } from '@hono/node-server/serve-static'
|
||||
import { Hono } from 'hono'
|
||||
import { createBootstrap } from './bootstrap'
|
||||
import { createLibsqlPlatform } from './platform/libsql'
|
||||
|
||||
const TURSO_DATABASE_URL = process.env.TURSO_DATABASE_URL
|
||||
if (!TURSO_DATABASE_URL) {
|
||||
throw new Error('TURSO_DATABASE_URL is required for the Azure Functions deployment.')
|
||||
}
|
||||
|
||||
const platform = await createLibsqlPlatform({
|
||||
TURSO_DATABASE_URL,
|
||||
TURSO_AUTH_TOKEN: process.env.TURSO_AUTH_TOKEN,
|
||||
})
|
||||
|
||||
const apiApp = await createBootstrap(platform)
|
||||
|
||||
const server = new Hono()
|
||||
server.route('/', apiApp)
|
||||
server.use('/*', serveStatic({ root: './dist' }))
|
||||
server.get('/*', serveStatic({ root: './dist', path: 'index.html' }))
|
||||
|
||||
app.http('zpan', {
|
||||
methods: ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'],
|
||||
authLevel: 'anonymous',
|
||||
route: '{*path}',
|
||||
handler: async (request: HttpRequest, _context: InvocationContext): Promise<Response> => {
|
||||
// Construct a standards-compliant Request so Hono body-reading works correctly
|
||||
// on POST/PUT/PATCH routes. Azure HttpRequest v4 has additional properties and
|
||||
// a single-read stream; wrapping it avoids silent breakage on body-reading routes.
|
||||
// The body cast and duplex option are required for Node 22's undici-based fetch.
|
||||
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
|
||||
const webReq = new Request(request.url, {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: hasBody ? (request.body as BodyInit) : undefined,
|
||||
...(hasBody ? { duplex: 'half' } : {}),
|
||||
} as RequestInit)
|
||||
return server.fetch(webReq)
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user