mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* Improve channel onboarding and Telegram pairing flow * fix: remove dead restart_required code, fix review findings, and stabilize polling E2E test - Remove restart_required/needs_restart dead code from 6 files (no real extension uses it; all channels hot-activate at runtime) - Remove dead extensions.configuredRestart i18n key from all 3 locales - Fix pairing test asserting wrong upsert semantics (test expected idempotent behavior but impl always rotates codes) - Fix pairing test using expired code for approval (req.code -> req_again.code) - Fix missing i18n fallback for auth.extensionTokenPlaceholder - Validate setup_url scheme (https?://) before assigning to <a>.href - Replace hardcoded English "Approve"/"Pairing code is required" with i18n keys - Demote misleading "bot is open to all users" Telegram log from Warn to Debug - Move polling E2E test to run first (polling loop dies during refresh_active_channel) - Add poll_interval_ms config field to Telegram WASM channel - Fix conversations.rs compilation (missing ? operator) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address remaining review comments (i18n regressions) - Remove hardcoded pairing_instructions() function from server.rs; use onboarding metadata from ExtensionManager instead (fixes i18n regression where pairing instructions were always English) - Restore i18n calls for stepper labels in renderWasmChannelStepper (was using hardcoded English strings instead of missions.step* keys) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restart polling loop on channel refresh, address Copilot review - Add WasmChannel::ensure_polling() that stops any stale polling task and starts a fresh one from the on_start config - Call ensure_polling() in refresh_active_channel after re-running on_start, fixing the root cause of the dead polling loop in E2E tests - Move polling test back to its original position (no longer order-dependent) - Fix requires_pairing in channel_onboarding_for_state to use channel_requires_pairing() instead of legacy owner_id-only check - Add rel='noopener noreferrer' to all setup_url target=_blank links Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: collapse nested if-let to satisfy clippy collapsible_if Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: return promise from approvePairing, stop polling unconditionally in ensure_polling - Add missing `return` before apiFetch in approvePairing() so callers can await/chain the result - Move poll_shutdown_tx.take() before the enabled check in ensure_polling() so switching from polling to webhook stops the old polling task Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove trailing commas in JSON test fixtures after restart_required removal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.0 KiB
Rust
104 lines
3.0 KiB
Rust
//! Shared helpers for one-time code verification flows.
|
|
//!
|
|
//! This module centralizes the common pieces used by code-based flows such as
|
|
//! DM pairing and any future manual verification flows:
|
|
//! - one-time code generation
|
|
//! - challenge presentation
|
|
//! - submission normalization
|
|
//! - pending challenge bookkeeping
|
|
|
|
use rand::Rng;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// User-facing payload for a code-based verification flow.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VerificationChallenge {
|
|
/// One-time code the user must send back to the integration.
|
|
pub code: String,
|
|
/// Human-readable instructions for completing verification.
|
|
pub instructions: String,
|
|
/// Deep-link or shortcut URL that prefills the verification payload when supported.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub deep_link: Option<String>,
|
|
}
|
|
|
|
/// Pending one-time challenge plus flow-specific metadata.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct PendingCodeChallenge<M> {
|
|
pub code: String,
|
|
pub meta: M,
|
|
pub expires_at_unix: u64,
|
|
}
|
|
|
|
impl<M> PendingCodeChallenge<M> {
|
|
pub fn new(code: String, meta: M, expires_at_unix: u64) -> Self {
|
|
Self {
|
|
code,
|
|
meta,
|
|
expires_at_unix,
|
|
}
|
|
}
|
|
|
|
pub fn is_expired(&self, now_unix: u64) -> bool {
|
|
self.expires_at_unix <= now_unix
|
|
}
|
|
}
|
|
|
|
/// Shared seam for code-driven verification flows.
|
|
pub trait CodeChallengeFlow {
|
|
type Meta: Clone;
|
|
|
|
/// Issue a new one-time code for this flow.
|
|
fn issue_code(&self) -> String;
|
|
|
|
/// Render user-facing instructions for a pending challenge.
|
|
fn render_challenge(&self, pending: &PendingCodeChallenge<Self::Meta>)
|
|
-> VerificationChallenge;
|
|
|
|
/// Normalize a submitted code before validation.
|
|
fn normalize_submission(&self, submission: &str) -> Option<String> {
|
|
normalize_submitted_code(submission)
|
|
}
|
|
|
|
/// Validate whether a submission satisfies the pending challenge.
|
|
fn matches_submission(
|
|
&self,
|
|
pending: &PendingCodeChallenge<Self::Meta>,
|
|
submission: &str,
|
|
) -> bool;
|
|
|
|
/// Build a pending challenge with a flow-generated code.
|
|
fn issue_challenge(
|
|
&self,
|
|
meta: Self::Meta,
|
|
expires_at_unix: u64,
|
|
) -> PendingCodeChallenge<Self::Meta> {
|
|
PendingCodeChallenge::new(self.issue_code(), meta, expires_at_unix)
|
|
}
|
|
}
|
|
|
|
/// Trim user input and reject blank codes before hitting storage.
|
|
pub fn normalize_submitted_code(submission: &str) -> Option<String> {
|
|
let trimmed = submission.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
}
|
|
|
|
/// Generate a fixed-length code from the provided alphabet.
|
|
pub fn generate_code(len: usize, alphabet: &[u8]) -> String {
|
|
if len == 0 || alphabet.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let mut rng = rand::thread_rng();
|
|
(0..len)
|
|
.map(|_| {
|
|
let idx = rng.gen_range(0..alphabet.len());
|
|
alphabet[idx] as char
|
|
})
|
|
.collect()
|
|
}
|