Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-test-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion backend/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,14 @@ export function verifyJwt(token: string): { publicKey: string } | null {
}

// Use timingSafeEqual to prevent timing attacks
if (providedSig.length !== expected.length || !crypto.timingSafeEqual(providedSig, expected)) {
// 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 {
return null;
}

Expand Down
14 changes: 14 additions & 0 deletions contracts/stream_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
21 changes: 11 additions & 10 deletions frontend/src/components/dashboard/dashboard-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,8 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
<p>Save recurring stream settings once, apply instantly, then override before submitting.</p>

<div className="stream-template-editor">
<input value={templateNameInput} onChange={(e) => setTemplateNameInput(e.target.value)} placeholder="e.g. Monthly Contributor Payroll" aria-label="Template name" />
<label htmlFor="template-name-input" style={{ position: 'absolute', width: '1px', height: '1px', padding: 0, margin: '-1px', overflow: 'hidden', clip: 'rect(0, 0, 0, 0)', whiteSpace: 'nowrap', borderWidth: 0 }}>Template name</label>
<input id="template-name-input" value={templateNameInput} onChange={(e) => setTemplateNameInput(e.target.value)} placeholder="e.g. Monthly Contributor Payroll" aria-label="Template name" />
<div className="stream-template-editor__actions">
<button type="button" className="secondary-button" disabled={!isTemplateNameValid} onClick={handleSaveTemplate}>{saveTemplateButtonLabel}</button>
{editingTemplateId ? <button type="button" className="secondary-button" onClick={handleClearTemplateEditor}>Stop Editing</button> : null}
Expand Down Expand Up @@ -918,28 +919,28 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
<h4>Stream Configuration</h4>
<p>{requiredFieldsCompleted} / 5 required fields completed</p>
</div>
<label className="stream-form__template-select">
<label className="stream-form__template-select" htmlFor="template-select">
Load template
<select value={selectedTemplateId ?? ""} onChange={(e) => { const id = e.target.value; if (!id) { setSelectedTemplateId(null); return; } handleApplyTemplate(id); }}>
<select id="template-select" value={selectedTemplateId ?? ""} onChange={(e) => { const id = e.target.value; if (!id) { setSelectedTemplateId(null); return; } handleApplyTemplate(id); }}>
<option value="">Select saved template</option>
{templates.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
</select>
</label>
</div>

<label>Recipient Address<input required type="text" value={streamForm.recipient} onChange={(e) => updateStreamForm("recipient", e.target.value)} placeholder="G..." /></label>
<label htmlFor="stream-recipient">Recipient Address<input id="stream-recipient" required type="text" value={streamForm.recipient} onChange={(e) => updateStreamForm("recipient", e.target.value)} placeholder="G..." /></label>
<div className="stream-form__row">
<label>Token<input required type="text" value={streamForm.token} onChange={(e) => updateStreamForm("token", e.target.value.toUpperCase())} placeholder="USDC" /></label>
<label>Total Amount<input required type="number" min="0" step="0.0000001" value={streamForm.totalAmount} onChange={(e) => updateStreamForm("totalAmount", e.target.value)} placeholder="100" /></label>
<label htmlFor="stream-token">Token<input id="stream-token" required type="text" value={streamForm.token} onChange={(e) => updateStreamForm("token", e.target.value.toUpperCase())} placeholder="USDC" /></label>
<label htmlFor="stream-total-amount">Total Amount<input id="stream-total-amount" required type="number" min="0" step="0.0000001" value={streamForm.totalAmount} onChange={(e) => updateStreamForm("totalAmount", e.target.value)} placeholder="100" /></label>
</div>
<div className="stream-form__row">
<label>Starts At<input required type="datetime-local" value={streamForm.startsAt} onChange={(e) => updateStreamForm("startsAt", e.target.value)} /></label>
<label>Ends At<input required type="datetime-local" value={streamForm.endsAt} onChange={(e) => updateStreamForm("endsAt", e.target.value)} /></label>
<label htmlFor="stream-starts-at">Starts At<input id="stream-starts-at" required type="datetime-local" value={streamForm.startsAt} onChange={(e) => updateStreamForm("startsAt", e.target.value)} /></label>
<label htmlFor="stream-ends-at">Ends At<input id="stream-ends-at" required type="datetime-local" value={streamForm.endsAt} onChange={(e) => updateStreamForm("endsAt", e.target.value)} /></label>
</div>
<div className="stream-form__row">
<label>Cadence (seconds)<input type="number" min="1" step="1" value={streamForm.cadenceSeconds} onChange={(e) => updateStreamForm("cadenceSeconds", e.target.value)} /></label>
<label htmlFor="stream-cadence">Cadence (seconds)<input id="stream-cadence" type="number" min="1" step="1" value={streamForm.cadenceSeconds} onChange={(e) => updateStreamForm("cadenceSeconds", e.target.value)} /></label>
</div>
<label>Note<textarea value={streamForm.note} onChange={(e) => updateStreamForm("note", e.target.value)} placeholder="Optional internal note for this stream configuration." /></label>
<label htmlFor="stream-note">Note<textarea id="stream-note" value={streamForm.note} onChange={(e) => updateStreamForm("note", e.target.value)} placeholder="Optional internal note for this stream configuration." /></label>

<div className="stream-form__actions">
<button type="submit" className="wallet-button" disabled={isFormSubmitting}>{isFormSubmitting ? "Submitting..." : "Create Stream"}</button>
Expand Down
Loading