diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9e8b1a4..54629fe4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "npm" cache-dependency-path: package-lock.json @@ -71,7 +71,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "npm" cache-dependency-path: package-lock.json diff --git a/.github/workflows/pr-test-gate.yml b/.github/workflows/pr-test-gate.yml index b2471627..d2b09b9e 100644 --- a/.github/workflows/pr-test-gate.yml +++ b/.github/workflows/pr-test-gate.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "npm" cache-dependency-path: package-lock.json diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 5f48309b..d78e277f 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20.19.0' + node-version: '22' cache: 'npm' - name: Install dependencies diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 9402a943..06e705c4 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -115,12 +115,24 @@ export function verifyJwt(token: string): { publicKey: string } | null { const providedSigBuf = Buffer.from(sig); const expectedSigBuf = Buffer.from(expectedSig); +<<<<<<< fix/jwt-signature-verification + // Use timingSafeEqual to prevent timing attacks + // This will throw if lengths differ, or return false if content differs + try { + const result = crypto.timingSafeEqual(providedSig, expected); + // timingSafeEqual returns true when equal, false when not equal (same length, different content) + if (result === false) { + return null; + } + } catch { +======= // Use timingSafeEqual to prevent timing attacks. Lengths are checked // first since timingSafeEqual throws on mismatched buffer lengths. if ( providedSigBuf.length !== expectedSigBuf.length || !crypto.timingSafeEqual(providedSigBuf, expectedSigBuf) ) { +>>>>>>> main return null; } diff --git a/contracts/stream_contract/src/test.rs b/contracts/stream_contract/src/test.rs index afb15ca1..e211c085 100644 --- a/contracts/stream_contract/src/test.rs +++ b/contracts/stream_contract/src/test.rs @@ -180,6 +180,20 @@ fn test_update_fee_config_rejects_invalid_fee_rate() { assert_eq!(result, Err(Ok(StreamError::InvalidFeeRate))); } +#[test] +fn test_update_fee_config_rejects_not_initialized() { + let env = Env::default(); + env.mock_all_auths(); + let client = create_contract(&env); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + + // Call update_fee_config before initialize + let result = client.try_update_fee_config(&admin, &treasury, &100); + assert_eq!(result, Err(Ok(StreamError::NotInitialized))); +} + #[test] fn test_initialize_emits_event() { let env = Env::default(); diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx index 5dac949f..4d0cb27a 100644 --- a/frontend/src/components/dashboard/dashboard-view.tsx +++ b/frontend/src/components/dashboard/dashboard-view.tsx @@ -884,7 +884,8 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {

Save recurring stream settings once, apply instantly, then override before submitting.

- setTemplateNameInput(e.target.value)} placeholder="e.g. Monthly Contributor Payroll" aria-label="Template name" /> + + setTemplateNameInput(e.target.value)} placeholder="e.g. Monthly Contributor Payroll" aria-label="Template name" />
{editingTemplateId ? : null} @@ -918,28 +919,28 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {

Stream Configuration

{requiredFieldsCompleted} / 5 required fields completed

-
- +
- - + +
- - + +
- +
-