Skip to content
Merged

Dev #538

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ jobs:
--health-timeout 3s
--health-retries 5

# The `Run migrations` step below targets DATABASE_URL, so the database
# it points at has to actually exist. Credentials/db name must match the
# DATABASE_URL declared under `env`.
postgres:
image: postgres:16-alpine
ports:
- 5432:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: clicked
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 3s
--health-retries 10

defaults:
run:
working-directory: apps/backend
Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/close-linked-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Close Linked Issues on Dev Merge

# GitHub only auto-closes "Closes #N" issues when a PR merges into the default
# branch (main). Since contributor PRs merge into `dev`, this workflow closes
# the linked issues at dev-merge time instead.

on:
pull_request_target:
types: [closed]
branches: [dev]

permissions:
issues: write

jobs:
close-linked-issues:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Close issues linked with closing keywords
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const text = `${pr.title}\n${pr.body || ''}`;

// Match closing keywords: close(s|d), fix(es|ed), resolve(s|d) + #N
const pattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+#(\d+)/gi;
const issueNumbers = [...new Set(
[...text.matchAll(pattern)].map((m) => Number(m[1]))
)];

if (issueNumbers.length === 0) {
core.info(`PR #${pr.number} has no linked issues with closing keywords.`);
return;
}

for (const issue_number of issueNumbers) {
try {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
});

// Skip PRs referenced by number, and issues already closed
if (issue.pull_request) {
core.info(`#${issue_number} is a PR, skipping.`);
continue;
}
if (issue.state === 'closed') {
core.info(`#${issue_number} is already closed, skipping.`);
continue;
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body: `Closed by #${pr.number}, merged into \`dev\`.`,
});

await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
state: 'closed',
state_reason: 'completed',
});

core.info(`Closed issue #${issue_number} (linked to PR #${pr.number}).`);
} catch (e) {
core.warning(`Could not close #${issue_number}: ${e.message}`);
}
}
75 changes: 75 additions & 0 deletions .github/workflows/guard-main-branch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Guard Main Branch

# Closes any PR targeting `main` that was not opened by the repo maintainer.
# Contributors must target the `dev` branch instead.
# Uses pull_request_target so the token has write access for PRs from forks.

on:
pull_request_target:
types: [opened, reopened, edited, ready_for_review]
branches: [main]

permissions:
pull-requests: write

jobs:
enforce-dev-target:
runs-on: ubuntu-latest
steps:
- name: Close non-maintainer PRs to main
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;

// Skip if the PR no longer targets main (relevant for `edited` events)
if (pr.base.ref !== 'main') {
core.info(`PR #${pr.number} targets ${pr.base.ref}, nothing to do.`);
return;
}

const author = pr.user.login;

// The repo owner is always allowed
let allowed = author === context.repo.owner;

// Also allow collaborators with admin or maintain permission
if (!allowed) {
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: author,
});
allowed = ['admin', 'maintain'].includes(data.permission);
} catch (e) {
core.info(`${author} is not a collaborator: treating as external contributor.`);
}
}

if (allowed) {
core.info(`PR #${pr.number} by ${author} is allowed to target main.`);
return;
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: [
`👋 Hi @${author}, thanks for your contribution!`,
'',
'Pull requests from contributors must target the **`dev`** branch — only the repo maintainer merges into `main`.',
'',
'This PR is being closed automatically. Please open a new PR (or retarget this one by reopening it after editing the base branch) against `dev`.',
].join('\n'),
});

await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed',
});

core.info(`Closed PR #${pr.number} by ${author} (targeted main).`);
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ apps/backend/.local-storage/
# Agent-tool isolated worktrees — internal tooling state, not project source.
.claude/worktrees/

# TypeScript build artifacts
# TypeScript build artifacts. `apps/backend/tsconfig.json` emits to dist/
# (already ignored above); these patterns remain only to keep a tree that
# predates that fix from re-staging stale emitted files.
apps/backend/src/**/*.js
apps/backend/src/**/*.js.map
apps/backend/src/**/*.d.ts
Expand Down
26 changes: 26 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
node_modules/
dist/
.next/
.turbo/
target/
coverage/

# ── Generated artifacts ───────────────────────────────────────────────────────
# All tool output, not hand-edited source. Reformatting them produces noisy
# diffs and they get rewritten on the next build/test run anyway.

# drizzle-kit migration snapshots and journal
apps/backend/drizzle/meta/

# TypeScript declaration output
**/*.d.ts

# Next.js ambient types
apps/web/next-env.d.ts

# Soroban test snapshots (regenerated by `cargo test`)
contracts/**/test_snapshots/

# Rust / Python build output
contracts/target/
apps/ai_agent/.venv/
15 changes: 15 additions & 0 deletions SECURITY_FIXES_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
### Frontend (Web App)

#### Modified Files

1. **`apps/web/src/lib/cryptoStore.ts`**
- ✅ Fixed identity key persistence using IndexedDB structured clone
- ✅ Private CryptoKey now persists across page reloads
Expand All @@ -21,6 +22,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
- ✅ Correct WebCrypto ECDH usage: `deriveBits(algo_with_peer_public, caller_private, bits)`

#### New Test Files

3. **`apps/web/src/lib/__tests__/ecdh-fix.test.ts`**
- ✅ Verifies Alice and Bob derive identical shared secrets
- ✅ Tests correct ECDH key usage
Expand All @@ -34,6 +36,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
### Backend

#### Modified Files

5. **`apps/backend/src/routes/uploads.ts`**
- ✅ Added SHA-256 integrity verification to upload confirmation
- ✅ Hash mismatch marks file as corrupted (deleted status)
Expand All @@ -53,6 +56,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
- ✅ Updated `dispatchOfflinePush` call to include `senderId`

#### New Files

9. **`apps/backend/src/services/pushFilter.ts`**
- ✅ NEW: Shared push recipient filtering logic
- ✅ Single source of truth for all push paths
Expand All @@ -64,6 +68,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
- ✅ Works with local storage, S3, MinIO

#### New Test Files

11. **`apps/backend/src/__tests__/pushFilter.test.ts`**
- ✅ Tests all filtering combinations
- ✅ Verifies sender filtering, mute checks, pushEnabled
Expand All @@ -80,6 +85,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
- ✅ Documents the complete security improvements

#### Documentation

14. **`apps/backend/docs/security-hardening.md`**
- ✅ Comprehensive documentation of all fixes
- ✅ Problem/solution for each issue
Expand All @@ -93,6 +99,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
**Problem**: `deriveSharedSecret` imported both keys as public keys, which is cryptographically invalid.

**Solution**:

- Updated signature to accept `callerPrivateKey: CryptoKey` and `peerPublicKeyJwk: JsonWebKey`
- Fixed `deriveBits` to use private key as base key
- Updated `establishSession` to pass identity private key
Expand All @@ -104,6 +111,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
**Problem**: Private keys were generated as non-extractable and discarded. `getIdentityPrivateKey` regenerated new keys on every call.

**Solution**:

- Generate keypairs with `extractable=true`
- Persist full `CryptoKeyPair` via IndexedDB structured clone
- Retrieve same private key across page reloads
Expand All @@ -115,6 +123,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
**Problem**: `dispatchOfflinePush` ignored `isMuted` and `pushEnabled`, while `sendPushForMessage` respected them.

**Solution**:

- Created shared `pushFilter.ts` with `getEligiblePushRecipients`
- Both push paths now use identical filtering logic
- Filters: sender, muted, online, pushEnabled, connection state
Expand All @@ -126,6 +135,7 @@ This implementation addresses four critical security vulnerabilities as a cohesi
**Problem**: Upload confirmation never verified SHA-256 hash; corrupted files could be marked ready.

**Solution**:

- Created `fileIntegrity.ts` with streaming SHA-256 computation
- Upload confirmation now verifies hash before marking ready
- Hash mismatch marks file as corrupted
Expand All @@ -135,18 +145,21 @@ This implementation addresses four critical security vulnerabilities as a cohesi
## Testing Coverage

### Unit Tests

- ✅ ECDH key agreement (Alice/Bob derive identical secrets)
- ✅ Identity persistence (keys survive page reloads)
- ✅ Push filtering (all combinations: mute, pushEnabled, online, connected)
- ✅ File integrity (hash computation, verification, tamper detection)

### Integration Tests

- ✅ End-to-end encrypted messaging flow
- ✅ Push notification consistency
- ✅ File upload with integrity verification
- ✅ Regression prevention

### Test Commands

```bash
# Frontend tests
cd apps/web
Expand All @@ -166,10 +179,12 @@ pnpm test fileIntegrity
## Backwards Compatibility

### Breaking Changes

- **ECDH**: `establishSession` now requires `myPrivateKey: CryptoKey` instead of `myPublicKey: JsonWebKey`
- **Migration**: Callers must retrieve private key via `cryptoStore.getIdentityPrivateKey()`

### Non-Breaking Changes

- **Identity Persistence**: Automatic migration; old keys continue working
- **Push Filter**: Fully backwards compatible; improved filtering
- **File Integrity**: Only applies to new uploads; existing files unaffected
Expand Down
Loading
Loading