diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index b5536eb34..b955e5014 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -22,6 +22,7 @@ on: permissions: contents: write + id-token: write jobs: build: @@ -58,6 +59,22 @@ jobs: fi echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + - name: Detect Windows signing eligibility + id: sign + shell: bash + run: | + # Sign Windows builds only on production v* tags (not beta-v*, not workflow_dispatch). + # This matches the single OIDC federated credential configured in Entra ID. + if [ "${{ matrix.os }}" = "windows-latest" ] \ + && [ "${{ github.event_name }}" = "push" ] \ + && [[ "$GITHUB_REF" == refs/tags/v* ]]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + echo "Windows signing: ENABLED (v* tag on windows-latest)" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "Windows signing: skipped" + fi + - name: Setup pnpm uses: pnpm/action-setup@v5 @@ -98,7 +115,31 @@ jobs: - name: Package & Publish shell: bash - run: pnpm exec electron-builder ${{ matrix.platform }} --config electron-builder.yml --publish ${{ inputs.publish || 'always' }} -c.extraMetadata.version=${{ steps.version.outputs.VERSION }} + run: | + CMD=(pnpm exec electron-builder ${{ matrix.platform }} \ + --config electron-builder.yml \ + --publish "${{ inputs.publish || 'always' }}" \ + -c.extraMetadata.version="${{ steps.version.outputs.VERSION }}") + + if [ "${{ steps.sign.outputs.enabled }}" = "true" ]; then + CMD+=(-c.win.azureSignOptions.publisherName="$WINDOWS_PUBLISHER_NAME") + CMD+=(-c.win.azureSignOptions.endpoint="$AZURE_CODESIGN_ENDPOINT") + CMD+=(-c.win.azureSignOptions.codeSigningAccountName="$AZURE_CODESIGN_ACCOUNT") + CMD+=(-c.win.azureSignOptions.certificateProfileName="$AZURE_CODESIGN_PROFILE") + fi + + "${CMD[@]}" working-directory: surfsense_desktop env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WINDOWS_PUBLISHER_NAME: ${{ vars.WINDOWS_PUBLISHER_NAME }} + AZURE_CODESIGN_ENDPOINT: ${{ vars.AZURE_CODESIGN_ENDPOINT }} + AZURE_CODESIGN_ACCOUNT: ${{ vars.AZURE_CODESIGN_ACCOUNT }} + AZURE_CODESIGN_PROFILE: ${{ vars.AZURE_CODESIGN_PROFILE }} + # Service principal credentials for Azure.Identity EnvironmentCredential used by the + # TrustedSigning PowerShell module. Only populated when signing is enabled. + # electron-builder 26 does not yet support OIDC federated tokens for Azure signing, + # so we fall back to client-secret auth. Rotate AZURE_CLIENT_SECRET before expiry. + AZURE_TENANT_ID: ${{ steps.sign.outputs.enabled == 'true' && secrets.AZURE_TENANT_ID || '' }} + AZURE_CLIENT_ID: ${{ steps.sign.outputs.enabled == 'true' && secrets.AZURE_CLIENT_ID || '' }} + AZURE_CLIENT_SECRET: ${{ steps.sign.outputs.enabled == 'true' && secrets.AZURE_CLIENT_SECRET || '' }} diff --git a/VERSION b/VERSION index e3b86dd9c..44517d518 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.16 +0.0.19 diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index 7dd6205d9..a1795853a 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -114,8 +114,19 @@ def _surfsense_error_handler(request: Request, exc: SurfSenseError) -> JSONRespo def _http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: - """Wrap FastAPI/Starlette HTTPExceptions into the standard envelope.""" + """Wrap FastAPI/Starlette HTTPExceptions into the standard envelope. + + 5xx sanitization policy: + - 500 responses are sanitized (replaced with ``GENERIC_5XX_MESSAGE``) because + they usually wrap raw internal errors and may leak sensitive info. + - Other 5xx statuses (501, 502, 503, 504, ...) are raised explicitly by + route code to communicate a specific, user-safe operational state + (e.g. 503 "Page purchases are temporarily unavailable."). Those details + are preserved so the frontend can render them, but the error is still + logged server-side. + """ rid = _get_request_id(request) + should_sanitize = exc.status_code == 500 # Structured dict details (e.g. {"code": "CAPTCHA_REQUIRED", "message": "..."}) # are preserved so the frontend can parse them. @@ -130,9 +141,9 @@ def _http_exception_handler(request: Request, exc: HTTPException) -> JSONRespons exc.status_code, message, ) - if exc.status_code == 500: - message = GENERIC_5XX_MESSAGE - err_code = "INTERNAL_ERROR" + if should_sanitize: + message = GENERIC_5XX_MESSAGE + err_code = "INTERNAL_ERROR" body = { "error": { "code": err_code, @@ -159,8 +170,8 @@ def _http_exception_handler(request: Request, exc: HTTPException) -> JSONRespons exc.status_code, detail, ) - if exc.status_code == 500: - detail = GENERIC_5XX_MESSAGE + if should_sanitize: + detail = GENERIC_5XX_MESSAGE code = _status_to_code(exc.status_code, detail) return _build_error_response(exc.status_code, detail, code=code, request_id=rid) diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml index 3a8c24a9a..01f5ddc1b 100644 --- a/surfsense_backend/pyproject.toml +++ b/surfsense_backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "surf-new-backend" -version = "0.0.16" +version = "0.0.19" description = "SurfSense Backend" requires-python = ">=3.12" dependencies = [ diff --git a/surfsense_backend/tests/unit/test_error_contract.py b/surfsense_backend/tests/unit/test_error_contract.py index 8a1605dd1..81ec08b2d 100644 --- a/surfsense_backend/tests/unit/test_error_contract.py +++ b/surfsense_backend/tests/unit/test_error_contract.py @@ -70,6 +70,20 @@ def _make_test_app(): async def raise_http_500(): raise HTTPException(status_code=500, detail="secret db password leaked") + @app.get("/http-503") + async def raise_http_503(): + raise HTTPException( + status_code=503, + detail="Page purchases are temporarily unavailable.", + ) + + @app.get("/http-502") + async def raise_http_502(): + raise HTTPException( + status_code=502, + detail="Unable to create Stripe checkout session.", + ) + @app.get("/surfsense-connector") async def raise_connector(): raise ConnectorError("GitHub API returned 401") @@ -184,6 +198,20 @@ class TestHTTPExceptionHandler: assert body["error"]["message"] == GENERIC_5XX_MESSAGE assert body["error"]["code"] == "INTERNAL_ERROR" + def test_503_preserves_detail(self, client): + # Intentional 503s (e.g. feature flag off) must surface the developer + # message so the frontend can render actionable copy. + body = _assert_envelope(client.get("/http-503"), 503) + assert ( + body["error"]["message"] == "Page purchases are temporarily unavailable." + ) + assert body["error"]["message"] != GENERIC_5XX_MESSAGE + + def test_502_preserves_detail(self, client): + body = _assert_envelope(client.get("/http-502"), 502) + assert body["error"]["message"] == "Unable to create Stripe checkout session." + assert body["error"]["message"] != GENERIC_5XX_MESSAGE + # --------------------------------------------------------------------------- # SurfSenseError hierarchy diff --git a/surfsense_backend/uv.lock b/surfsense_backend/uv.lock index 2c846c60e..ac2784668 100644 --- a/surfsense_backend/uv.lock +++ b/surfsense_backend/uv.lock @@ -7947,7 +7947,7 @@ wheels = [ [[package]] name = "surf-new-backend" -version = "0.0.16" +version = "0.0.19" source = { editable = "." } dependencies = [ { name = "alembic" }, diff --git a/surfsense_browser_extension/package.json b/surfsense_browser_extension/package.json index 487025a8d..146dd177e 100644 --- a/surfsense_browser_extension/package.json +++ b/surfsense_browser_extension/package.json @@ -1,7 +1,7 @@ { "name": "surfsense_browser_extension", "displayName": "Surfsense Browser Extension", - "version": "0.0.16", + "version": "0.0.19", "description": "Extension to collect Browsing History for SurfSense.", "author": "https://github.com/MODSetter", "engines": { diff --git a/surfsense_desktop/package.json b/surfsense_desktop/package.json index f099d732b..638fd3ffc 100644 --- a/surfsense_desktop/package.json +++ b/surfsense_desktop/package.json @@ -1,6 +1,6 @@ { "name": "surfsense-desktop", - "version": "0.0.16", + "version": "0.0.19", "description": "SurfSense Desktop App", "main": "dist/main.js", "scripts": { diff --git a/surfsense_web/app/(home)/free/[model_slug]/page.tsx b/surfsense_web/app/(home)/free/[model_slug]/page.tsx index 9b7a7e0b6..cc06376d1 100644 --- a/surfsense_web/app/(home)/free/[model_slug]/page.tsx +++ b/surfsense_web/app/(home)/free/[model_slug]/page.tsx @@ -41,7 +41,7 @@ async function getAllModels(): Promise { function buildSeoTitle(model: AnonModel): string { if (model.seo_title) return model.seo_title; - return `${model.name} Free Online Without Login | No Sign-Up AI Chat | SurfSense`; + return `Chat with ${model.name} Free, No Login | SurfSense`; } function buildSeoDescription(model: AnonModel): string { diff --git a/surfsense_web/app/(home)/free/page.tsx b/surfsense_web/app/(home)/free/page.tsx index 00ad5e0e5..8d9ed5cb1 100644 --- a/surfsense_web/app/(home)/free/page.tsx +++ b/surfsense_web/app/(home)/free/page.tsx @@ -18,7 +18,7 @@ import type { AnonModel } from "@/contracts/types/anonymous-chat.types"; import { BACKEND_URL } from "@/lib/env-config"; export const metadata: Metadata = { - title: "ChatGPT Free Online Without Login | Chat GPT No Login, Claude AI Free | SurfSense", + title: "Free AI Chat, No Login Required | SurfSense", description: "Use ChatGPT free online without login. Chat with GPT-4, Claude AI, Gemini and more for free. No sign-up required. Open source NotebookLM alternative with free AI chat and document Q&A.", keywords: [ @@ -67,7 +67,7 @@ export const metadata: Metadata = { canonical: "https://surfsense.com/free", }, openGraph: { - title: "ChatGPT Free Online Without Login | Claude AI Free No Login | SurfSense", + title: "Free AI Chat, No Login Required | SurfSense", description: "Use ChatGPT free online without login. Chat with GPT-4, Claude AI, Gemini and 100+ AI models. Open source NotebookLM alternative.", url: "https://surfsense.com/free", @@ -84,7 +84,7 @@ export const metadata: Metadata = { }, twitter: { card: "summary_large_image", - title: "ChatGPT Free Online Without Login | Claude AI Free No Login | SurfSense", + title: "Free AI Chat, No Login Required | SurfSense", description: "Use ChatGPT free online without login. Chat with GPT-4, Claude AI, Gemini and more. No sign-up needed.", images: ["/og-image.png"], diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 4e6930094..144968a2b 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -45,7 +45,7 @@ export const metadata: Metadata = { alternates: { canonical: "https://surfsense.com", }, - title: "SurfSense - NotebookLM Alternative | Free ChatGPT & Claude AI", + title: "SurfSense – Open Source, Privacy-Focused NotebookLM Alternative for Teams", description: "Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.", keywords: [ @@ -87,7 +87,7 @@ export const metadata: Metadata = { "SurfSense", ], openGraph: { - title: "SurfSense - NotebookLM Alternative | Free ChatGPT & Claude AI", + title: "SurfSense – Open Source, Privacy-Focused NotebookLM Alternative for Teams", description: "Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude, and any AI model for free.", url: "https://surfsense.com", @@ -105,7 +105,7 @@ export const metadata: Metadata = { }, twitter: { card: "summary_large_image", - title: "SurfSense - NotebookLM Alternative | Free ChatGPT & Claude AI", + title: "SurfSense – Open Source, Privacy-Focused NotebookLM Alternative for Teams", description: "Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.", creator: "@SurfSenseAI", diff --git a/surfsense_web/package.json b/surfsense_web/package.json index 58da8933a..a98c21f83 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -1,6 +1,6 @@ { "name": "surfsense_web", - "version": "0.0.16", + "version": "0.0.19", "private": true, "description": "SurfSense Frontend", "scripts": {