feat(安全): 实现零知识金库架构,密钥以 SSE 密文落库,会话层以密文传输,仅在 OTP 生成或导出的瞬间于内存中解密,用完即焚。 #79
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy to Cloudflare Workers | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| name: Deploy | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Check Required Secrets | |
| env: | |
| NODEAUTH_LICENSE: ${{ secrets.NODEAUTH_LICENSE }} | |
| JWT_SECRET: ${{ secrets.JWT_SECRET }} | |
| ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} | |
| OAUTH_ALLOWED_USERS: ${{ secrets.OAUTH_ALLOWED_USERS }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_D1_DATABASE_ID: ${{ secrets.CLOUDFLARE_D1_DATABASE_ID }} | |
| run: | | |
| REQUIRED_VARS=( | |
| "NODEAUTH_LICENSE" | |
| "JWT_SECRET" | |
| "ENCRYPTION_KEY" | |
| "OAUTH_ALLOWED_USERS" | |
| "CLOUDFLARE_API_TOKEN" | |
| "CLOUDFLARE_ACCOUNT_ID" | |
| "CLOUDFLARE_D1_DATABASE_ID" | |
| ) | |
| MISSING_VARS=() | |
| for VAR in "${REQUIRED_VARS[@]}"; do | |
| if [ -z "${!VAR}" ]; then | |
| MISSING_VARS+=("$VAR") | |
| fi | |
| done | |
| if [ ${#MISSING_VARS[@]} -ne 0 ]; then | |
| echo "❌ Error: The following required secrets are missing or empty:" | |
| for VAR in "${MISSING_VARS[@]}"; do | |
| echo " - $VAR" | |
| done | |
| echo "Please configure them in your repository's Settings > Secrets and variables > Actions." | |
| exit 1 | |
| fi | |
| echo "✅ All required secrets are present." | |
| - name: Prepare Wrangler Config | |
| env: | |
| CONFIG_FILE: ${{ env.WRANGLER_CONFIG || 'wrangler.toml' }} | |
| run: | | |
| cp wrangler.toml wrangler.toml.bak | |
| if [ "$CONFIG_FILE" != "wrangler.toml" ]; then | |
| cp "$CONFIG_FILE" wrangler.toml | |
| fi | |
| # 注入 Cloudflare Workers 名称 | |
| - name: Inject Workers Name | |
| env: | |
| CONFIG_FILE: ${{ env.WRANGLER_CONFIG || 'wrangler.toml' }} | |
| CLOUDFLARE_WORKERS_NAME: ${{ secrets.CLOUDFLARE_WORKERS_NAME || 'nodeauth-worker' }} | |
| run: | | |
| sed -i "0,/name =/s/name = \".*\"/name = \"$CLOUDFLARE_WORKERS_NAME\"/" "wrangler.toml" | |
| - name: Inject Global Configurations | |
| env: | |
| WORKERS_DEV: ${{ secrets.CLOUDFLARE_WORKERS_DEV }} | |
| PREVIEW_URLS: ${{ secrets.CLOUDFLARE_WORKERS_PREVIEW_URLS }} | |
| ROUTES: ${{ secrets.CLOUDFLARE_WORKERS_ROUTES }} | |
| run: | | |
| if [ -n "$WORKERS_DEV" ]; then | |
| sed -i '/^workers_dev[[:space:]]*=/d' wrangler.toml | |
| sed -i "1i workers_dev = $WORKERS_DEV" wrangler.toml | |
| fi | |
| if [ -n "$PREVIEW_URLS" ]; then | |
| sed -i '/^preview_urls[[:space:]]*=/d' wrangler.toml | |
| sed -i "1i preview_urls = $PREVIEW_URLS" wrangler.toml | |
| fi | |
| if [ -n "$ROUTES" ]; then | |
| sed -i '/^\[\[routes\]\]/d; /^pattern[[:space:]]*=[[:space:]]*".*"/d; /^zone_name[[:space:]]*=[[:space:]]*".*"/d' wrangler.toml | |
| IFS=';' read -ra ADDR <<< "$ROUTES" | |
| for row in "${ADDR[@]}"; do | |
| IFS=',' read -ra PARTS <<< "$row" | |
| echo -e "\n[[routes]]\npattern = \"${PARTS[0]}\"" >> wrangler.toml | |
| [ ${#PARTS[@]} -ge 2 ] && echo "zone_name = \"${PARTS[1]}\"" >> wrangler.toml | |
| done | |
| fi | |
| - name: Inject D1 Database ID | |
| env: | |
| CONFIG_FILE: ${{ env.WRANGLER_CONFIG || 'wrangler.toml' }} | |
| CLOUDFLARE_D1_DATABASE_ID: ${{ secrets.CLOUDFLARE_D1_DATABASE_ID }} | |
| CLOUDFLARE_D1_DATABASE_NAME: ${{ secrets.CLOUDFLARE_D1_DATABASE_NAME || 'nodeauth-db' }} | |
| run: | | |
| sed -i '/^\[\[d1_databases\]\]/d; /^[[:space:]]*database_name[[:space:]]*=/d; /^[[:space:]]*database_id[[:space:]]*=/d; /^[[:space:]]*binding[[:space:]]*=[[:space:]]*"DB"/d' wrangler.toml | |
| printf "\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"$CLOUDFLARE_D1_DATABASE_NAME\"\ndatabase_id = \"$CLOUDFLARE_D1_DATABASE_ID\"\n" >> wrangler.toml | |
| - name: Sync Database Schema | |
| uses: cloudflare/wrangler-action@v3 | |
| with: | |
| apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| wranglerVersion: '4.75.0' | |
| command: d1 execute ${{ secrets.CLOUDFLARE_D1_DATABASE_NAME || 'nodeauth-db' }} --remote --file=backend/schema.sql | |
| - name: Inject Commit Hash & Platform | |
| run: | | |
| SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) | |
| echo "Injecting Public SHA: $SHORT_SHA (Platform: Cloudflare Workers)" | |
| # 1. Backend Injection | |
| sed -i "s/__DIST_COMMIT_HASH__/$SHORT_SHA/g" backend/dist/server.js || true | |
| sed -i "s/__DIST_COMMIT_HASH__/$SHORT_SHA/g" backend/dist/worker.js || true | |
| sed -i "s/__DIST_PLATFORM__/Cloudflare Workers/g" backend/dist/worker.js || true | |
| # 2. Frontend Injection: Icons & Manifest | |
| sed -i "s/__DIST_ICON_SUFFIX__/cloudflare/g" frontend/dist/index.html || true | |
| sed -i "s/__DIST_ICON_SUFFIX__/cloudflare/g" frontend/dist/manifest.webmanifest || true | |
| find frontend/dist -name "*.js" -exec sed -i "s/__DIST_ICON_SUFFIX__/cloudflare/g" {} + || true | |
| # 3. Frontend Injection: Version & Platform Info | |
| find frontend/dist -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i "s/__DIST_COMMIT_HASH__/$SHORT_SHA/g" {} + || true | |
| find frontend/dist -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i "s/__DIST_PLATFORM__/Cloudflare Workers/g" {} + || true | |
| - name: Generate Secret List | |
| id: secret_list | |
| env: | |
| OAUTH_GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }} | |
| OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }} | |
| OAUTH_GITHUB_REDIRECT_URI: ${{ secrets.OAUTH_GITHUB_REDIRECT_URI }} | |
| OAUTH_CLOUDFLARE_CLIENT_ID: ${{ secrets.OAUTH_CLOUDFLARE_CLIENT_ID }} | |
| OAUTH_CLOUDFLARE_CLIENT_SECRET: ${{ secrets.OAUTH_CLOUDFLARE_CLIENT_SECRET }} | |
| OAUTH_CLOUDFLARE_ORG_DOMAIN: ${{ secrets.OAUTH_CLOUDFLARE_ORG_DOMAIN }} | |
| OAUTH_CLOUDFLARE_REDIRECT_URI: ${{ secrets.OAUTH_CLOUDFLARE_REDIRECT_URI }} | |
| OAUTH_NODELOC_CLIENT_ID: ${{ secrets.OAUTH_NODELOC_CLIENT_ID }} | |
| OAUTH_NODELOC_CLIENT_SECRET: ${{ secrets.OAUTH_NODELOC_CLIENT_SECRET }} | |
| OAUTH_NODELOC_REDIRECT_URI: ${{ secrets.OAUTH_NODELOC_REDIRECT_URI }} | |
| OAUTH_GITEE_CLIENT_ID: ${{ secrets.OAUTH_GITEE_CLIENT_ID }} | |
| OAUTH_GITEE_CLIENT_SECRET: ${{ secrets.OAUTH_GITEE_CLIENT_SECRET }} | |
| OAUTH_GITEE_REDIRECT_URI: ${{ secrets.OAUTH_GITEE_REDIRECT_URI }} | |
| OAUTH_TELEGRAM_BOT_NAME: ${{ secrets.OAUTH_TELEGRAM_BOT_NAME }} | |
| OAUTH_TELEGRAM_BOT_TOKEN: ${{ secrets.OAUTH_TELEGRAM_BOT_TOKEN }} | |
| OAUTH_TELEGRAM_WEBHOOK_SECRET: ${{ secrets.OAUTH_TELEGRAM_WEBHOOK_SECRET }} | |
| OAUTH_GOOGLE_CLIENT_ID: ${{ secrets.OAUTH_GOOGLE_CLIENT_ID }} | |
| OAUTH_GOOGLE_CLIENT_SECRET: ${{ secrets.OAUTH_GOOGLE_CLIENT_SECRET }} | |
| OAUTH_GOOGLE_REDIRECT_URI: ${{ secrets.OAUTH_GOOGLE_REDIRECT_URI }} | |
| OAUTH_MICROSOFT_CLIENT_ID: ${{ secrets.OAUTH_MICROSOFT_CLIENT_ID }} | |
| OAUTH_MICROSOFT_CLIENT_SECRET: ${{ secrets.OAUTH_MICROSOFT_CLIENT_SECRET }} | |
| OAUTH_BAIDU_CLIENT_ID: ${{ secrets.OAUTH_BAIDU_CLIENT_ID }} | |
| OAUTH_BAIDU_CLIENT_SECRET: ${{ secrets.OAUTH_BAIDU_CLIENT_SECRET }} | |
| OAUTH_DROPBOX_CLIENT_ID: ${{ secrets.OAUTH_DROPBOX_CLIENT_ID }} | |
| OAUTH_DROPBOX_CLIENT_SECRET: ${{ secrets.OAUTH_DROPBOX_CLIENT_SECRET }} | |
| OAUTH_WALLETCONNECT_PROJECT_ID: ${{ secrets.OAUTH_WALLETCONNECT_PROJECT_ID }} | |
| OAUTH_WALLETCONNECT_SELF_PROXY: ${{ secrets.OAUTH_WALLETCONNECT_SELF_PROXY }} | |
| run: | | |
| SECRETS="NODEAUTH_LICENSE | |
| OAUTH_ALLOW_ALL | |
| OAUTH_ALLOWED_USERS | |
| ENCRYPTION_KEY | |
| JWT_SECRET" | |
| OPTIONAL_VARS=( | |
| "OAUTH_GITHUB_CLIENT_ID" "OAUTH_GITHUB_CLIENT_SECRET" "OAUTH_GITHUB_REDIRECT_URI" | |
| "OAUTH_CLOUDFLARE_CLIENT_ID" "OAUTH_CLOUDFLARE_CLIENT_SECRET" "OAUTH_CLOUDFLARE_ORG_DOMAIN" "OAUTH_CLOUDFLARE_REDIRECT_URI" | |
| "OAUTH_NODELOC_CLIENT_ID" "OAUTH_NODELOC_CLIENT_SECRET" "OAUTH_NODELOC_REDIRECT_URI" | |
| "OAUTH_GITEE_CLIENT_ID" "OAUTH_GITEE_CLIENT_SECRET" "OAUTH_GITEE_REDIRECT_URI" | |
| "OAUTH_TELEGRAM_BOT_NAME" "OAUTH_TELEGRAM_BOT_TOKEN" "OAUTH_TELEGRAM_WEBHOOK_SECRET" | |
| "OAUTH_GOOGLE_CLIENT_ID" "OAUTH_GOOGLE_CLIENT_SECRET" "OAUTH_GOOGLE_REDIRECT_URI" | |
| "OAUTH_MICROSOFT_CLIENT_ID" "OAUTH_MICROSOFT_CLIENT_SECRET" | |
| "OAUTH_BAIDU_CLIENT_ID" "OAUTH_BAIDU_CLIENT_SECRET" | |
| "OAUTH_DROPBOX_CLIENT_ID" "OAUTH_DROPBOX_CLIENT_SECRET" | |
| "OAUTH_WALLETCONNECT_PROJECT_ID" "OAUTH_WALLETCONNECT_SELF_PROXY" | |
| ) | |
| for VAR in "${OPTIONAL_VARS[@]}"; do | |
| if [ -n "${!VAR}" ]; then SECRETS="$SECRETS | |
| $VAR"; fi | |
| done | |
| echo "secrets<<EOF" >> $GITHUB_OUTPUT | |
| echo "$SECRETS" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Mask Sensitive Domain | |
| run: | | |
| if [ -n "${{ secrets.CLOUDFLARE_WORKER_SUBDOMAIN }}" ]; then | |
| echo "::add-mask::${{ secrets.CLOUDFLARE_WORKER_SUBDOMAIN }}" | |
| fi | |
| - name: Deploy to Cloudflare Workers | |
| uses: cloudflare/wrangler-action@v3 | |
| with: | |
| apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| wranglerVersion: '4.75.0' | |
| command: deploy --minify | |
| secrets: ${{ steps.secret_list.outputs.secrets }} | |
| env: | |
| OAUTH_ALLOW_ALL: ${{ secrets.OAUTH_ALLOW_ALL || 'false' }} | |
| OAUTH_ALLOWED_USERS: ${{ secrets.OAUTH_ALLOWED_USERS }} | |
| OAUTH_GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }} | |
| OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }} | |
| OAUTH_GITHUB_REDIRECT_URI: ${{ secrets.OAUTH_GITHUB_REDIRECT_URI }} | |
| OAUTH_CLOUDFLARE_CLIENT_ID: ${{ secrets.OAUTH_CLOUDFLARE_CLIENT_ID }} | |
| OAUTH_CLOUDFLARE_CLIENT_SECRET: ${{ secrets.OAUTH_CLOUDFLARE_CLIENT_SECRET }} | |
| OAUTH_CLOUDFLARE_ORG_DOMAIN: ${{ secrets.OAUTH_CLOUDFLARE_ORG_DOMAIN }} | |
| OAUTH_CLOUDFLARE_REDIRECT_URI: ${{ secrets.OAUTH_CLOUDFLARE_REDIRECT_URI }} | |
| OAUTH_NODELOC_CLIENT_ID: ${{ secrets.OAUTH_NODELOC_CLIENT_ID }} | |
| OAUTH_NODELOC_CLIENT_SECRET: ${{ secrets.OAUTH_NODELOC_CLIENT_SECRET }} | |
| OAUTH_NODELOC_REDIRECT_URI: ${{ secrets.OAUTH_NODELOC_REDIRECT_URI }} | |
| OAUTH_GITEE_CLIENT_ID: ${{ secrets.OAUTH_GITEE_CLIENT_ID }} | |
| OAUTH_GITEE_CLIENT_SECRET: ${{ secrets.OAUTH_GITEE_CLIENT_SECRET }} | |
| OAUTH_GITEE_REDIRECT_URI: ${{ secrets.OAUTH_GITEE_REDIRECT_URI }} | |
| OAUTH_TELEGRAM_BOT_NAME: ${{ secrets.OAUTH_TELEGRAM_BOT_NAME }} | |
| OAUTH_TELEGRAM_BOT_TOKEN: ${{ secrets.OAUTH_TELEGRAM_BOT_TOKEN }} | |
| OAUTH_TELEGRAM_WEBHOOK_SECRET: ${{ secrets.OAUTH_TELEGRAM_WEBHOOK_SECRET }} | |
| OAUTH_GOOGLE_CLIENT_ID: ${{ secrets.OAUTH_GOOGLE_CLIENT_ID }} | |
| OAUTH_GOOGLE_CLIENT_SECRET: ${{ secrets.OAUTH_GOOGLE_CLIENT_SECRET }} | |
| OAUTH_GOOGLE_REDIRECT_URI: ${{ secrets.OAUTH_GOOGLE_REDIRECT_URI }} | |
| OAUTH_MICROSOFT_CLIENT_ID: ${{ secrets.OAUTH_MICROSOFT_CLIENT_ID }} | |
| OAUTH_MICROSOFT_CLIENT_SECRET: ${{ secrets.OAUTH_MICROSOFT_CLIENT_SECRET }} | |
| OAUTH_BAIDU_CLIENT_ID: ${{ secrets.OAUTH_BAIDU_CLIENT_ID }} | |
| OAUTH_BAIDU_CLIENT_SECRET: ${{ secrets.OAUTH_BAIDU_CLIENT_SECRET }} | |
| OAUTH_DROPBOX_CLIENT_ID: ${{ secrets.OAUTH_DROPBOX_CLIENT_ID }} | |
| OAUTH_DROPBOX_CLIENT_SECRET: ${{ secrets.OAUTH_DROPBOX_CLIENT_SECRET }} | |
| OAUTH_WALLETCONNECT_PROJECT_ID: ${{ secrets.OAUTH_WALLETCONNECT_PROJECT_ID }} | |
| OAUTH_WALLETCONNECT_SELF_PROXY: ${{ secrets.OAUTH_WALLETCONNECT_SELF_PROXY }} | |
| ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} | |
| JWT_SECRET: ${{ secrets.JWT_SECRET }} | |
| NODEAUTH_LICENSE: ${{ secrets.NODEAUTH_LICENSE }} | |
| - name: Restore Wrangler Config | |
| if: always() | |
| run: | | |
| if [ -f wrangler.toml.bak ]; then mv wrangler.toml.bak wrangler.toml; fi |