fix(android): send the credentials a request can actually present (Phase B: native auth parity) #5361
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: Security Tests | |
| on: | |
| push: | |
| branches: [master, develop, pu/flash-sandbox] | |
| paths: | |
| - 'packages/lib/src/security/**' | |
| - 'packages/lib/src/services/sandbox/**' | |
| - 'packages/db/src/schema/sandbox-sessions.ts' | |
| - 'packages/db/drizzle/**' | |
| - 'apps/web/src/lib/ai/tools/sandbox-tools.ts' | |
| - 'apps/web/src/lib/ai/tools/sandbox-tools-runtime.ts' | |
| - 'apps/web/src/lib/ai/core/ai-tools.ts' | |
| - 'packages/lib/src/auth/**' | |
| - 'packages/lib/src/permissions/**' | |
| - 'packages/lib/src/repositories/**' | |
| - 'packages/lib/src/__tests__/security-test-utils.ts' | |
| - 'packages/lib/src/__tests__/test-fixtures/security-fixtures.ts' | |
| - 'packages/lib/src/__tests__/**/*(security|auth|csrf|permission|token|rate-limit|broadcast|encryption|multi-tenant)*.ts' | |
| - 'packages/db/src/schema/auth.ts' | |
| - 'packages/db/src/schema/sessions.ts' | |
| - 'packages/db/src/transactions/**' | |
| - 'apps/web/src/lib/auth/**' | |
| - 'apps/web/src/app/api/auth/**' | |
| - 'apps/web/src/middleware/**' | |
| - 'apps/processor/src/middleware/auth*' | |
| - 'apps/processor/src/utils/security*' | |
| - 'apps/realtime/src/**/*auth*' | |
| - 'scripts/test-security.sh' | |
| - '.github/workflows/security.yml' | |
| - 'apps/web/src/app/api/**/route.ts' | |
| - 'apps/web/src/app/api/__tests__/security-audit-coverage.test.ts' | |
| # Surfaces that only scripts/test-security.sh covers. Without these the | |
| # script would be wired in and still dark for the code it uniquely guards: | |
| # the OAuth 2.1 provider routes/repository, the MCP WebSocket route, the | |
| # AI tools' SSRF boundary, and the CLI/SDK credential + token paths. | |
| - 'apps/web/src/app/api/oauth/**' | |
| - 'apps/web/src/app/api/mcp-ws/**' | |
| - 'apps/web/src/lib/ai/tools/**' | |
| - 'apps/web/src/lib/repositories/**' | |
| - 'packages/cli/**' | |
| - 'packages/sdk/**' | |
| # The agent-workspace layer. Its compare-and-swap suite | |
| # (`agent-sessions-store.integration.test.ts`) is excluded from | |
| # @pagespace/lib's default config and matched by no other config, so the | |
| # `test:db` step below is its ONLY runner anywhere — and a path filter | |
| # that omitted the code under test meant a PR touching only | |
| # `agent-sessions-store.ts` never triggered this workflow and never ran | |
| # it. The tool-layer authz gates (`session-tools*.ts`) live under | |
| # `apps/web/src/lib/ai/tools/**`, already listed above. | |
| - 'packages/lib/src/services/agent-workspaces/**' | |
| - 'packages/lib/src/agent-workspaces/**' | |
| - 'apps/web/src/lib/agent-workspaces/**' | |
| # The published-app hosting layer, same story. Its claim/transition suite | |
| # (`provisioner-claim.integration.test.ts`) is excluded from | |
| # @pagespace/lib's default config and matched by no other config, so the | |
| # `test:db` step below is its ONLY runner anywhere. `published_apps` rows | |
| # are claimed by concurrent workers and gated by CHECK constraints — both | |
| # Postgres guarantees — and without these entries a PR touching only the | |
| # provisioner would never trigger this workflow and never run them. | |
| - 'packages/lib/src/services/app-hosting/**' | |
| - 'packages/lib/src/services/fly/**' | |
| - 'packages/db/src/schema/published-apps.ts' | |
| pull_request: | |
| # BASE branches, not head. This branch listed itself here and it bought | |
| # nothing: its PR targets `master`, which is already listed, so the suite | |
| # was already running on every push (and did — every commit shows one | |
| # `event=pull_request` Security Tests run and no second one). The entry | |
| # would only matter for a PR *targeting* this branch, and there are none. | |
| # Removed rather than left as a line that provably does nothing in a | |
| # security workflow. See test.yml, where the same misreading cost real | |
| # capacity rather than merely being inert. | |
| branches: [master, pu/flash-sandbox] | |
| paths: | |
| - 'packages/lib/src/security/**' | |
| - 'packages/lib/src/services/sandbox/**' | |
| - 'packages/db/src/schema/sandbox-sessions.ts' | |
| - 'packages/db/drizzle/**' | |
| - 'apps/web/src/lib/ai/tools/sandbox-tools.ts' | |
| - 'apps/web/src/lib/ai/tools/sandbox-tools-runtime.ts' | |
| - 'apps/web/src/lib/ai/core/ai-tools.ts' | |
| - 'packages/lib/src/auth/**' | |
| - 'packages/lib/src/permissions/**' | |
| - 'packages/lib/src/repositories/**' | |
| - 'packages/lib/src/__tests__/security-test-utils.ts' | |
| - 'packages/lib/src/__tests__/test-fixtures/security-fixtures.ts' | |
| - 'packages/lib/src/__tests__/**/*(security|auth|csrf|permission|token|rate-limit|broadcast|encryption|multi-tenant)*.ts' | |
| - 'packages/db/src/schema/auth.ts' | |
| - 'packages/db/src/schema/sessions.ts' | |
| - 'packages/db/src/transactions/**' | |
| - 'apps/web/src/lib/auth/**' | |
| - 'apps/web/src/app/api/auth/**' | |
| - 'apps/web/src/middleware/**' | |
| - 'apps/processor/src/middleware/auth*' | |
| - 'apps/processor/src/utils/security*' | |
| - 'apps/realtime/src/**/*auth*' | |
| - 'scripts/test-security.sh' | |
| - '.github/workflows/security.yml' | |
| - 'apps/web/src/app/api/**/route.ts' | |
| - 'apps/web/src/app/api/__tests__/security-audit-coverage.test.ts' | |
| # Surfaces that only scripts/test-security.sh covers. Without these the | |
| # script would be wired in and still dark for the code it uniquely guards: | |
| # the OAuth 2.1 provider routes/repository, the MCP WebSocket route, the | |
| # AI tools' SSRF boundary, and the CLI/SDK credential + token paths. | |
| - 'apps/web/src/app/api/oauth/**' | |
| - 'apps/web/src/app/api/mcp-ws/**' | |
| - 'apps/web/src/lib/ai/tools/**' | |
| - 'apps/web/src/lib/repositories/**' | |
| - 'packages/cli/**' | |
| - 'packages/sdk/**' | |
| # The agent-workspace layer. Its compare-and-swap suite | |
| # (`agent-sessions-store.integration.test.ts`) is excluded from | |
| # @pagespace/lib's default config and matched by no other config, so the | |
| # `test:db` step below is its ONLY runner anywhere — and a path filter | |
| # that omitted the code under test meant a PR touching only | |
| # `agent-sessions-store.ts` never triggered this workflow and never ran | |
| # it. The tool-layer authz gates (`session-tools*.ts`) live under | |
| # `apps/web/src/lib/ai/tools/**`, already listed above. | |
| - 'packages/lib/src/services/agent-workspaces/**' | |
| - 'packages/lib/src/agent-workspaces/**' | |
| - 'apps/web/src/lib/agent-workspaces/**' | |
| # The published-app hosting layer, same story. Its claim/transition suite | |
| # (`provisioner-claim.integration.test.ts`) is excluded from | |
| # @pagespace/lib's default config and matched by no other config, so the | |
| # `test:db` step below is its ONLY runner anywhere. `published_apps` rows | |
| # are claimed by concurrent workers and gated by CHECK constraints — both | |
| # Postgres guarantees — and without these entries a PR touching only the | |
| # provisioner would never trigger this workflow and never run them. | |
| - 'packages/lib/src/services/app-hosting/**' | |
| - 'packages/lib/src/services/fly/**' | |
| - 'packages/db/src/schema/published-apps.ts' | |
| # Allow manual trigger | |
| workflow_dispatch: | |
| # Run on schedule for continuous security monitoring | |
| schedule: | |
| # Run at 6:00 UTC every day | |
| - cron: '0 6 * * *' | |
| jobs: | |
| security-tests: | |
| name: Security Test Suite | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:17-alpine | |
| env: | |
| POSTGRES_DB: pagespace_test | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Build workspace packages | |
| run: bun run --filter '@pagespace/db' build && bun run --filter '@pagespace/lib' build | |
| - name: Run database migrations | |
| run: bun run db:migrate | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| - name: Run core security tests | |
| run: bun run --filter '@pagespace/lib' test -- src/security/ | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run sandbox (code-execution) tests | |
| run: bun run --filter '@pagespace/lib' test -- src/services/sandbox/ | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run auth module tests | |
| run: bun run --filter '@pagespace/lib' test -- src/auth/ | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run security utility tests | |
| run: | | |
| bun run --filter '@pagespace/lib' test -- src/__tests__/secure-compare.test.ts | |
| bun run --filter '@pagespace/lib' test -- src/__tests__/csrf-utils.test.ts | |
| bun run --filter '@pagespace/lib' test -- src/__tests__/token-utils.test.ts | |
| bun run --filter '@pagespace/lib' test -- src/__tests__/token-lookup.test.ts | |
| bun run --filter '@pagespace/lib' test -- src/__tests__/broadcast-auth.test.ts | |
| bun run --filter '@pagespace/lib' test -- src/__tests__/encryption-utils.test.ts | |
| bun run --filter '@pagespace/lib' test:db -- src/__tests__/device-auth-utils.test.ts | |
| bun run --filter '@pagespace/lib' test -- src/__tests__/rate-limit-utils.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run multi-tenant isolation tests | |
| run: bun run --filter '@pagespace/lib' test -- src/__tests__/multi-tenant-isolation.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| # The broadcast claim is the one mechanism preventing a mass double-send, and its | |
| # guarantee is Postgres's (row locking under ON CONFLICT DO UPDATE ... WHERE), not | |
| # ours. A mocked test can only pin the SQL we emit, so this races the real statement | |
| # against itself on a live database. | |
| - name: Run broadcast claim concurrency tests | |
| run: bun run --filter '@pagespace/lib' test:db -- src/services/broadcast/__tests__/claim-recipient.integration.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| # The agent-session store's compare-and-swap predicates are Postgres's | |
| # guarantee, not ours: a fake compares with `===` (which handles null for | |
| # free) while SQL needs `IS NULL`, so a mocked test cannot tell a correct | |
| # CAS from one that silently matches nothing. This runs them for real. | |
| - name: Run agent-session store integration tests | |
| run: bun run --filter '@pagespace/lib' test:db -- src/services/agent-workspaces/__tests__/agent-sessions-store.integration.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| # The published-app claim is what stops two workers provisioning the same | |
| # app on Fly, and its guarantee is Postgres's: FOR UPDATE SKIP LOCKED plus | |
| # a claim written inside the locking transaction. A mocked test can only | |
| # pin the statements we emit — it cannot show that a second worker gets | |
| # nothing after the first transaction COMMITS, which is the entire point. | |
| # The same file checks that every transition the pure core allows is one | |
| # the status CHECK constraints accept, by attempting each refused write. | |
| - name: Run published-app claim and transition integration tests | |
| run: bun run --filter '@pagespace/lib' test:db -- src/services/app-hosting/__tests__/provisioner-claim.integration.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run permission tests | |
| run: | | |
| bun run --filter '@pagespace/lib' test:db -- src/__tests__/permissions.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| # THESE RAN NOWHERE. All five are excluded from @pagespace/lib's default | |
| # vitest config (they need a live Postgres) and no workflow or script | |
| # named them, so `zero-trust-boundaries` and `cross-tenant-escalation` — | |
| # the two suites whose entire subject is the authorization boundary — had | |
| # no runner at all. `test:db` uses vitest.security.config.ts, which sets | |
| # no `include` and therefore only runs the files passed as arguments; | |
| # being excluded from the default config is not the same as being run | |
| # somewhere else, and nothing was closing that gap. | |
| # | |
| # The two auth service files are the same story with an extra twist: the | |
| # "web app auth library tests" step below runs `src/auth/` through the | |
| # DEFAULT config, which excludes exactly these two, so that step steps | |
| # right over them. | |
| - name: Run zero-trust and cross-tenant boundary tests | |
| run: | | |
| bun run --filter '@pagespace/lib' test:db -- \ | |
| src/permissions/__tests__/zero-trust-boundaries.test.ts \ | |
| src/permissions/__tests__/permission-mutations.test.ts \ | |
| src/__tests__/cross-tenant-escalation.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run auth service tests (magic link, passkey) | |
| run: | | |
| bun run --filter '@pagespace/lib' test:db -- \ | |
| src/auth/magic-link-service.test.ts \ | |
| src/auth/passkey-service.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run web app auth route tests | |
| run: bun run --filter 'web' test -- src/app/api/auth/__tests__/ | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run web app auth library tests | |
| run: bun run --filter 'web' test -- src/lib/auth/__tests__/ | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run security headers tests | |
| run: bun run --filter 'web' test -- src/middleware/__tests__/security-headers.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run database transaction security tests | |
| run: bun run --filter '@pagespace/db' test -- src/transactions/__tests__/auth-transactions.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run processor security tests | |
| run: bun run --filter '@pagespace/processor' test -- tests/security-utils.test.ts | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| - name: Run SDK and CLI package suites (auth providers, credential store, single-auth-path) | |
| run: | | |
| bun run --filter '@pagespace/sdk' test:coverage | |
| bun run --filter '@pagespace/cli' test:coverage | |
| env: | |
| NODE_ENV: test | |
| - name: Run security audit route coverage gate | |
| run: bun run --filter 'web' test -- src/app/api/__tests__/security-audit-coverage.test.ts | |
| env: | |
| NODE_ENV: test | |
| # scripts/test-security.sh was repaired to 51/51 green at the start of the | |
| # agent-session epic and then invoked by no workflow, so nothing it uniquely | |
| # covers has ever been gated: the OAuth 2.1 provider routes and repository, | |
| # the MCP WebSocket route, the AI tools' SSRF boundary (web_fetch redirect / | |
| # DNS-rebind), Google One Tap and open-redirect protection, the magic-link and | |
| # passkey route suites, device-fingerprint utils, and the security-test-utils | |
| # self-check. It is not a superset of the steps above (those add the sandbox | |
| # suites, the broadcast-claim and agent-session-store concurrency tests, and | |
| # whole-directory runs of packages/lib/src/auth and the web auth routes) and | |
| # the steps above are not a superset of it — so it runs IN ADDITION rather | |
| # than replacing them. The overlap costs runtime; the gap cost signal. | |
| # | |
| # DATABASE_URL is set so the script uses this job's Postgres service instead | |
| # of trying to `docker compose up` its own (its local-dev path). | |
| - name: Run scripts/test-security.sh (suites no step above covers) | |
| run: bun run test:security | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/pagespace_test | |
| NODE_ENV: test | |
| dependency-audit: | |
| name: Dependency Audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Run OSV vulnerability scan | |
| uses: google/osv-scanner-action/osv-scanner-action@6e4298ebc4db23e847df9b2e2de2939d6f066c67 | |
| with: | |
| scan-args: |- | |
| --recursive | |
| ./ | |
| # Pre-existing vulnerabilities (electron, next, multer) are tracked | |
| # separately. Set to false once those are remediated. | |
| continue-on-error: true | |
| secret-scanning: | |
| name: Secret Scanning | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: TruffleHog Secret Scan | |
| uses: trufflesecurity/trufflehog@v3.97.1 | |
| with: | |
| path: ./ | |
| base: ${{ github.event.before }} | |
| head: ${{ github.sha }} | |
| extra_args: --only-verified | |
| static-analysis: | |
| name: Static Security Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: TypeScript type checking | |
| run: bun run typecheck | |
| - name: ESLint security rules | |
| run: bun run --filter 'web' lint | |
| codeql-analysis: | |
| name: CodeQL Security Analysis | |
| runs-on: ubuntu-latest | |
| permissions: | |
| security-events: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@v4.37.9 | |
| with: | |
| languages: javascript-typescript | |
| queries: security-extended | |
| - name: Perform CodeQL Analysis | |
| uses: github/codeql-action/analyze@v4.37.9 | |
| with: | |
| category: "/language:javascript-typescript" | |
| security-summary: | |
| name: Security Summary | |
| runs-on: ubuntu-latest | |
| needs: [security-tests, dependency-audit, secret-scanning, static-analysis, codeql-analysis] | |
| if: always() | |
| steps: | |
| - name: Check all security jobs | |
| run: | | |
| echo "Security Test Suite Summary" | |
| echo "===========================" | |
| echo "" | |
| echo "Security Tests: ${{ needs.security-tests.result }}" | |
| echo "Dependency Audit: ${{ needs.dependency-audit.result }}" | |
| echo "Secret Scanning: ${{ needs.secret-scanning.result }}" | |
| echo "Static Analysis: ${{ needs.static-analysis.result }}" | |
| echo "CodeQL Analysis: ${{ needs.codeql-analysis.result }}" | |
| echo "" | |
| # Fail if any critical job failed | |
| if [ "${{ needs.security-tests.result }}" == "failure" ]; then | |
| echo "❌ Security tests failed" | |
| exit 1 | |
| fi | |
| if [ "${{ needs.dependency-audit.result }}" == "failure" ]; then | |
| echo "❌ Dependency audit failed (critical vulnerabilities found)" | |
| exit 1 | |
| fi | |
| if [ "${{ needs.secret-scanning.result }}" == "failure" ]; then | |
| echo "❌ Secret scanning found exposed secrets" | |
| exit 1 | |
| fi | |
| echo "✅ All critical security checks passed" |