Skip to content

fix(android): send the credentials a request can actually present (Phase B: native auth parity) #5361

fix(android): send the credentials a request can actually present (Phase B: native auth parity)

fix(android): send the credentials a request can actually present (Phase B: native auth parity) #5361

Workflow file for this run

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"