use qrcode::{EcLevel, QrCode};
#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
pub struct QrCodeMatrix {
pub size: u32,
pub modules: String,
}
#[uniffi::export]
pub fn encode_text_qr(text: String) -> Option<QrCodeMatrix> {
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
let code = QrCode::with_error_correction_level(trimmed.as_bytes(), EcLevel::M).ok()?;
let width = code.width();
let cells = code.to_colors();
let mut modules = String::with_capacity(width * width);
for color in cells {
modules.push(if color == qrcode::Color::Dark {
'1'
} else {
'0'
});
}
Some(QrCodeMatrix {
size: width as u32,
modules,
})
}
#[uniffi::export]
pub fn is_device_approval_bootstrap(raw: String) -> bool {
nostr_identity::parse_nostr_identity_device_approval_bootstrap(&raw, &[])
.ok()
.flatten()
.is_some()
}
#[cfg(test)]
mod tests {
use super::is_device_approval_bootstrap;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use nostr_identity::{
create_nostr_identity_device_approval_request,
encode_nostr_identity_device_approval_bootstrap, nostr_identity_device_approval_bootstrap,
CreateNostrIdentityDeviceApprovalRequestOptions,
};
use nostr_sdk::Keys;
#[test]
fn device_approval_bootstrap_rejects_non_bootstrap_inputs() {
assert!(!is_device_approval_bootstrap("".into()));
assert!(!is_device_approval_bootstrap("npub1plainvalue".into()));
assert!(!is_device_approval_bootstrap("https://example.com".into()));
assert!(!is_device_approval_bootstrap(
"nostr-identity://device-approval/?app_key=npub1legacy".into()
));
assert!(!is_device_approval_bootstrap(format!(
"{}.{}.not-base64!*",
"1".repeat(64),
"1".repeat(64)
)));
}
#[test]
fn device_link_qr_accepts_only_shared_bootstrap() {
let device = Keys::generate();
let request = Keys::generate();
let local = create_nostr_identity_device_approval_request(
&device,
CreateNostrIdentityDeviceApprovalRequestOptions {
request_keys: Some(request),
request_secret: Some("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE".to_string()),
requested_at: 41,
request_type: Some("device_link".to_string()),
resources: Vec::new(),
expires_at: None,
profile_id: None,
admin_app_key_pubkey: None,
label: Some("Safari on macOS".to_string()),
},
)
.expect("approval request");
let bootstrap =
nostr_identity_device_approval_bootstrap(&local.request).expect("bootstrap");
let encoded = encode_nostr_identity_device_approval_bootstrap(&bootstrap, None)
.expect("encode bootstrap");
let legacy_full_request = format!(
"nostr-identity://device-approval/{}",
URL_SAFE_NO_PAD
.encode(serde_json::to_vec(&local.request).expect("legacy full request JSON"))
);
assert!(is_device_approval_bootstrap(encoded.clone()));
assert!(!is_device_approval_bootstrap(legacy_full_request));
assert_eq!(
serde_json::to_value(&bootstrap)
.expect("bootstrap JSON")
.as_object()
.expect("bootstrap object")
.keys()
.cloned()
.collect::<std::collections::BTreeSet<_>>(),
["deviceAppKeyNpub", "label", "requestNpub", "requestSecret"]
.into_iter()
.map(str::to_string)
.collect()
);
assert_eq!(bootstrap.label.as_deref(), Some("Safari on macOS"));
let prefixed = format!("prefix:{encoded}");
assert!(!is_device_approval_bootstrap(prefixed));
}
}