Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
684 lines
21 KiB
Rust
684 lines
21 KiB
Rust
#![allow(clippy::too_many_lines)]
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use napi::Task;
|
|
use napi::bindgen_prelude::{
|
|
AsyncTask, Buffer, Env, Error, JsObjectValue, Object, Result, Status, Unknown,
|
|
};
|
|
use napi::{JsValue, ValueType};
|
|
use napi_derive::napi;
|
|
|
|
mod common;
|
|
use common::{
|
|
CreateInput, CreateResult, DescriptorInput, GetInput, GetResult, attachment_from_transport,
|
|
};
|
|
|
|
#[cfg(target_os = "linux")]
|
|
mod linux;
|
|
#[cfg(target_os = "macos")]
|
|
mod macos;
|
|
#[cfg(target_os = "windows")]
|
|
mod windows;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
const BACKEND_NAME: &str = "macos-authenticationservices";
|
|
#[cfg(target_os = "windows")]
|
|
const BACKEND_NAME: &str = "windows-webauthn";
|
|
#[cfg(target_os = "linux")]
|
|
const BACKEND_NAME: &str = "linux-libfido2";
|
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
|
const BACKEND_NAME: &str = "unavailable";
|
|
|
|
#[cfg(target_os = "macos")]
|
|
const BACKEND_REASON: &str = "macOS WebAuthn backend (AuthenticationServices). \
|
|
Requires macOS 12.0+ (ASAuthorizationPlatformPublicKeyCredentialProvider, \
|
|
introduced in Monterey). On older macOS the bundled \
|
|
ASAuthorizationPlatform/SecurityKey provider classes are not present and \
|
|
`isSupported()` returns false.";
|
|
#[cfg(target_os = "windows")]
|
|
const BACKEND_REASON: &str = "Windows WebAuthn broker backend (webauthn.dll). \
|
|
Requires Windows 10 build 18362 (May 2019 / version 1903) or newer; \
|
|
webauthn.dll is delay-loaded via /DELAYLOAD, so the addon still loads on \
|
|
older builds but `isSupported()` returns false there.";
|
|
#[cfg(target_os = "linux")]
|
|
const BACKEND_REASON: &str = "Linux WebAuthn backend (libfido2). Requires \
|
|
libfido2 >= 1.5.0 (HID transport, U2F fallback disabled) and a CTAP \
|
|
authenticator on USB-HID; `isSupported()` returns false when no \
|
|
authenticator is enumerated.";
|
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
|
const BACKEND_REASON: &str = "no WebAuthn backend is compiled in for this platform";
|
|
|
|
const TARGET_PLATFORM: &str = if cfg!(target_os = "macos") {
|
|
"darwin"
|
|
} else if cfg!(target_os = "windows") {
|
|
"win32"
|
|
} else if cfg!(target_os = "linux") {
|
|
"linux"
|
|
} else {
|
|
"unknown"
|
|
};
|
|
|
|
const TARGET_ARCH: &str = if cfg!(target_arch = "x86_64") {
|
|
"x64"
|
|
} else if cfg!(target_arch = "aarch64") {
|
|
"arm64"
|
|
} else {
|
|
"unknown"
|
|
};
|
|
|
|
fn target_string() -> String {
|
|
format!("{TARGET_PLATFORM}/{TARGET_ARCH}")
|
|
}
|
|
|
|
fn platform_broker_available() -> bool {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
windows::api_version() > 0
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
linux::is_supported()
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
macos::is_supported()
|
|
}
|
|
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
|
{
|
|
false
|
|
}
|
|
}
|
|
|
|
fn platform_authenticator_available() -> bool {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
windows::is_user_verifying_platform_authenticator_available()
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
false
|
|
}
|
|
}
|
|
|
|
fn ceremonies_implemented() -> bool {
|
|
cfg!(any(
|
|
target_os = "macos",
|
|
target_os = "windows",
|
|
target_os = "linux"
|
|
))
|
|
}
|
|
|
|
fn is_supported_native() -> bool {
|
|
ceremonies_implemented() && platform_broker_available()
|
|
}
|
|
|
|
fn api_version() -> u32 {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
windows::api_version()
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
0
|
|
}
|
|
}
|
|
|
|
fn backend_reason() -> String {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
format!("{} {}", BACKEND_REASON, linux::support_diagnostics())
|
|
}
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
BACKEND_REASON.to_owned()
|
|
}
|
|
}
|
|
|
|
#[napi(object, js_name = "WebAuthnBackendInfo")]
|
|
pub struct WebAuthnBackendInfoJs {
|
|
pub target: String,
|
|
pub backend: String,
|
|
#[napi(js_name = "nativeLoaded")]
|
|
pub native_loaded: bool,
|
|
#[napi(js_name = "ceremoniesImplemented")]
|
|
pub ceremonies_implemented: bool,
|
|
#[napi(js_name = "platformBrokerAvailable")]
|
|
pub platform_broker_available: bool,
|
|
#[napi(js_name = "platformAuthenticatorAvailable")]
|
|
pub platform_authenticator_available: bool,
|
|
pub supported: bool,
|
|
#[napi(js_name = "apiVersion")]
|
|
pub api_version: u32,
|
|
pub reason: String,
|
|
}
|
|
|
|
#[napi(js_name = "getBackendInfo")]
|
|
pub fn get_backend_info() -> WebAuthnBackendInfoJs {
|
|
WebAuthnBackendInfoJs {
|
|
target: target_string(),
|
|
backend: BACKEND_NAME.to_owned(),
|
|
native_loaded: true,
|
|
ceremonies_implemented: ceremonies_implemented(),
|
|
platform_broker_available: platform_broker_available(),
|
|
platform_authenticator_available: platform_authenticator_available(),
|
|
supported: is_supported_native(),
|
|
api_version: api_version(),
|
|
reason: backend_reason(),
|
|
}
|
|
}
|
|
|
|
pub struct IsSupportedTask;
|
|
impl Task for IsSupportedTask {
|
|
type Output = bool;
|
|
type JsValue = bool;
|
|
fn compute(&mut self) -> Result<Self::Output> {
|
|
Ok(is_supported_native())
|
|
}
|
|
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
#[napi(js_name = "isSupported")]
|
|
pub fn is_supported() -> AsyncTask<IsSupportedTask> {
|
|
AsyncTask::new(IsSupportedTask)
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct CreateCeremonyResultJs {
|
|
#[napi(js_name = "rawId")]
|
|
pub raw_id: Buffer,
|
|
#[napi(js_name = "attestationObject")]
|
|
pub attestation_object: Buffer,
|
|
#[napi(js_name = "clientDataJSON")]
|
|
pub client_data_json: Buffer,
|
|
|
|
#[napi(js_name = "authenticatorAttachment")]
|
|
pub authenticator_attachment: Option<String>,
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct GetCeremonyResultJs {
|
|
#[napi(js_name = "rawId")]
|
|
pub raw_id: Buffer,
|
|
#[napi(js_name = "authenticatorData")]
|
|
pub authenticator_data: Buffer,
|
|
pub signature: Buffer,
|
|
|
|
#[napi(js_name = "userHandle")]
|
|
pub user_handle: Option<Buffer>,
|
|
#[napi(js_name = "clientDataJSON")]
|
|
pub client_data_json: Buffer,
|
|
#[napi(js_name = "authenticatorAttachment")]
|
|
pub authenticator_attachment: Option<String>,
|
|
}
|
|
|
|
pub struct CreateTask {
|
|
input: CreateInput,
|
|
}
|
|
|
|
impl Task for CreateTask {
|
|
type Output = CreateResult;
|
|
type JsValue = CreateCeremonyResultJs;
|
|
|
|
fn compute(&mut self) -> Result<Self::Output> {
|
|
run_create(&mut self.input)
|
|
}
|
|
|
|
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
|
|
Ok(CreateCeremonyResultJs {
|
|
raw_id: Buffer::from(output.raw_id),
|
|
attestation_object: Buffer::from(output.attestation_object),
|
|
client_data_json: Buffer::from(output.client_data_json),
|
|
authenticator_attachment: attachment_from_transport(output.used_transport)
|
|
.map(str::to_owned),
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct GetTask {
|
|
input: GetInput,
|
|
}
|
|
|
|
impl Task for GetTask {
|
|
type Output = GetResult;
|
|
type JsValue = GetCeremonyResultJs;
|
|
|
|
fn compute(&mut self) -> Result<Self::Output> {
|
|
run_get(&mut self.input)
|
|
}
|
|
|
|
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
|
|
Ok(GetCeremonyResultJs {
|
|
raw_id: Buffer::from(output.raw_id),
|
|
authenticator_data: Buffer::from(output.authenticator_data),
|
|
signature: Buffer::from(output.signature),
|
|
user_handle: output.user_handle.map(Buffer::from),
|
|
client_data_json: Buffer::from(output.client_data_json),
|
|
authenticator_attachment: attachment_from_transport(output.used_transport)
|
|
.map(str::to_owned),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn run_create(input: &mut CreateInput) -> Result<CreateResult> {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
windows::make_credential(input)
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
linux::make_credential(input)
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
macos::make_credential(input)
|
|
}
|
|
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
|
{
|
|
let _ = input;
|
|
Err(common::ceremony_error(
|
|
common::CREATE_PREFIX,
|
|
"UnknownPlatform",
|
|
))
|
|
}
|
|
}
|
|
|
|
fn run_get(input: &mut GetInput) -> Result<GetResult> {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
windows::get_assertion(input)
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
linux::get_assertion(input)
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
macos::get_assertion(input)
|
|
}
|
|
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
|
{
|
|
let _ = input;
|
|
Err(common::ceremony_error(
|
|
common::GET_PREFIX,
|
|
"UnknownPlatform",
|
|
))
|
|
}
|
|
}
|
|
|
|
#[napi(js_name = "create")]
|
|
pub fn create(options: Object) -> Result<AsyncTask<CreateTask>> {
|
|
let input = parse_create_input(&options)?;
|
|
Ok(AsyncTask::new(CreateTask { input }))
|
|
}
|
|
|
|
#[napi(js_name = "get")]
|
|
pub fn get(options: Object) -> Result<AsyncTask<GetTask>> {
|
|
let input = parse_get_input(&options)?;
|
|
Ok(AsyncTask::new(GetTask { input }))
|
|
}
|
|
|
|
fn invalid_create_options() -> Error {
|
|
Error::new(
|
|
Status::InvalidArg,
|
|
"invalid WebAuthn registration options".to_owned(),
|
|
)
|
|
}
|
|
|
|
fn invalid_get_options() -> Error {
|
|
Error::new(
|
|
Status::InvalidArg,
|
|
"invalid WebAuthn assertion options".to_owned(),
|
|
)
|
|
}
|
|
|
|
fn parse_create_input(object: &Object) -> Result<CreateInput> {
|
|
let map_err = |_e: Error| invalid_create_options();
|
|
Ok(CreateInput {
|
|
rp_id: required_string(object, "rpId").map_err(map_err)?,
|
|
rp_name: required_string(object, "rpName").map_err(map_err)?,
|
|
challenge: required_buffer(object, "challenge").map_err(map_err)?,
|
|
user_id: required_buffer(object, "userId").map_err(map_err)?,
|
|
user_name: required_string(object, "userName").map_err(map_err)?,
|
|
user_display_name: required_string(object, "userDisplayName").map_err(map_err)?,
|
|
client_data_json: required_buffer(object, "clientDataJSON").map_err(map_err)?,
|
|
client_data_hash: required_buffer(object, "clientDataHash").map_err(map_err)?,
|
|
pub_key_algs: required_alg_array(object).map_err(map_err)?,
|
|
exclude_credentials: optional_descriptor_array(object, "excludeCredentials")
|
|
.map_err(map_err)?,
|
|
timeout_ms: optional_u32(object, "timeout", 0).map_err(map_err)?,
|
|
authenticator_attachment: optional_u32(
|
|
object,
|
|
"authenticatorAttachment",
|
|
default_attachment(),
|
|
)
|
|
.map_err(map_err)?,
|
|
user_verification: optional_u32(object, "userVerification", default_user_verification())
|
|
.map_err(map_err)?,
|
|
attestation: optional_u32(object, "attestation", default_attestation()).map_err(map_err)?,
|
|
enterprise_attestation: optional_u32(
|
|
object,
|
|
"enterpriseAttestation",
|
|
common::ENTERPRISE_NONE,
|
|
)
|
|
.map_err(map_err)?,
|
|
require_resident_key: optional_bool(object, "requireResidentKey", false)
|
|
.map_err(map_err)?,
|
|
prefer_resident_key: optional_bool(object, "preferResidentKey", false).map_err(map_err)?,
|
|
window_handle: optional_window_handle(object).map_err(map_err)?,
|
|
pin: optional_string(object, "pin").map_err(map_err)?,
|
|
})
|
|
}
|
|
|
|
fn parse_get_input(object: &Object) -> Result<GetInput> {
|
|
let map_err = |_e: Error| invalid_get_options();
|
|
Ok(GetInput {
|
|
rp_id: required_string(object, "rpId").map_err(map_err)?,
|
|
challenge: required_buffer(object, "challenge").map_err(map_err)?,
|
|
client_data_json: required_buffer(object, "clientDataJSON").map_err(map_err)?,
|
|
client_data_hash: required_buffer(object, "clientDataHash").map_err(map_err)?,
|
|
allow_credentials: optional_descriptor_array(object, "allowCredentials")
|
|
.map_err(map_err)?,
|
|
timeout_ms: optional_u32(object, "timeout", 0).map_err(map_err)?,
|
|
authenticator_attachment: optional_u32(
|
|
object,
|
|
"authenticatorAttachment",
|
|
default_attachment(),
|
|
)
|
|
.map_err(map_err)?,
|
|
user_verification: optional_u32(object, "userVerification", default_user_verification())
|
|
.map_err(map_err)?,
|
|
window_handle: optional_window_handle(object).map_err(map_err)?,
|
|
pin: optional_string(object, "pin").map_err(map_err)?,
|
|
})
|
|
}
|
|
|
|
fn optional_string(object: &Object, key: &str) -> Result<Option<String>> {
|
|
let Some(value) = read_unknown(object, key) else {
|
|
return Ok(None);
|
|
};
|
|
if is_nullish(&value) {
|
|
return Ok(None);
|
|
}
|
|
if value.get_type()? != ValueType::String {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} must be a string"),
|
|
));
|
|
}
|
|
let s: String = unsafe { value.cast::<String>()? };
|
|
if s.is_empty() { Ok(None) } else { Ok(Some(s)) }
|
|
}
|
|
|
|
fn optional_window_handle(object: &Object) -> Result<u64> {
|
|
let Some(value) = read_unknown(object, "windowHandle") else {
|
|
return Ok(0);
|
|
};
|
|
if is_nullish(&value) {
|
|
return Ok(0);
|
|
}
|
|
let bytes = read_buffer_bytes(&value, "windowHandle")?;
|
|
if bytes.len() > 8 {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
"windowHandle must be at most 8 bytes".to_owned(),
|
|
));
|
|
}
|
|
let mut padded = [0u8; 8];
|
|
padded[..bytes.len()].copy_from_slice(&bytes);
|
|
Ok(u64::from_le_bytes(padded))
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn default_attachment() -> u32 {
|
|
common::ATTACHMENT_ANY
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
fn default_user_verification() -> u32 {
|
|
common::USER_VERIFICATION_PREFERRED
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
fn default_attestation() -> u32 {
|
|
common::ATTESTATION_NONE
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn default_attachment() -> u32 {
|
|
0
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn default_user_verification() -> u32 {
|
|
0
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn default_attestation() -> u32 {
|
|
0
|
|
}
|
|
|
|
fn read_unknown<'a>(object: &Object<'a>, key: &str) -> Option<Unknown<'a>> {
|
|
object.get::<Unknown>(key).ok().flatten()
|
|
}
|
|
|
|
fn is_nullish(v: &Unknown<'_>) -> bool {
|
|
match v.get_type() {
|
|
Ok(t) => t == ValueType::Null || t == ValueType::Undefined,
|
|
Err(_) => true,
|
|
}
|
|
}
|
|
|
|
fn required_string(object: &Object, key: &str) -> Result<String> {
|
|
let value = read_unknown(object, key)
|
|
.ok_or_else(|| Error::new(Status::InvalidArg, format!("missing {key}")))?;
|
|
if is_nullish(&value) {
|
|
return Err(Error::new(Status::InvalidArg, format!("missing {key}")));
|
|
}
|
|
if value.get_type()? != ValueType::String {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} must be a string"),
|
|
));
|
|
}
|
|
let s: String = unsafe { value.cast::<String>()? };
|
|
Ok(s)
|
|
}
|
|
|
|
fn optional_u32(object: &Object, key: &str, default: u32) -> Result<u32> {
|
|
let Some(value) = read_unknown(object, key) else {
|
|
return Ok(default);
|
|
};
|
|
if is_nullish(&value) {
|
|
return Ok(default);
|
|
}
|
|
if value.get_type()? != ValueType::Number {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} must be a number"),
|
|
));
|
|
}
|
|
let n: u32 = unsafe { value.cast::<u32>()? };
|
|
Ok(n)
|
|
}
|
|
|
|
fn optional_bool(object: &Object, key: &str, default: bool) -> Result<bool> {
|
|
let Some(value) = read_unknown(object, key) else {
|
|
return Ok(default);
|
|
};
|
|
if is_nullish(&value) {
|
|
return Ok(default);
|
|
}
|
|
if value.get_type()? != ValueType::Boolean {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} must be a boolean"),
|
|
));
|
|
}
|
|
let b: bool = unsafe { value.cast::<bool>()? };
|
|
Ok(b)
|
|
}
|
|
|
|
fn required_buffer(object: &Object, key: &str) -> Result<Vec<u8>> {
|
|
let value = read_unknown(object, key)
|
|
.ok_or_else(|| Error::new(Status::InvalidArg, format!("missing {key}")))?;
|
|
if is_nullish(&value) {
|
|
return Err(Error::new(Status::InvalidArg, format!("missing {key}")));
|
|
}
|
|
read_buffer_bytes(&value, key)
|
|
}
|
|
|
|
fn read_buffer_bytes(value: &Unknown<'_>, key: &str) -> Result<Vec<u8>> {
|
|
let raw = value.raw();
|
|
let raw_env = value.value().env;
|
|
let mut is_buffer = false;
|
|
let status = unsafe { napi::sys::napi_is_buffer(raw_env, raw, &mut is_buffer) };
|
|
if status != napi::sys::Status::napi_ok || !is_buffer {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} must be a Buffer"),
|
|
));
|
|
}
|
|
let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
|
|
let mut len: usize = 0;
|
|
let status = unsafe { napi::sys::napi_get_buffer_info(raw_env, raw, &mut data_ptr, &mut len) };
|
|
if status != napi::sys::Status::napi_ok {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} buffer read failed"),
|
|
));
|
|
}
|
|
if len == 0 {
|
|
return Ok(Vec::new());
|
|
}
|
|
if data_ptr.is_null() {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} buffer pointer was null"),
|
|
));
|
|
}
|
|
|
|
let slice = unsafe { std::slice::from_raw_parts(data_ptr.cast::<u8>(), len) };
|
|
Ok(slice.to_vec())
|
|
}
|
|
|
|
fn required_alg_array(object: &Object) -> Result<Vec<i32>> {
|
|
let Some(value) = read_unknown(object, "pubKeyCredParams") else {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
"missing pubKeyCredParams".to_owned(),
|
|
));
|
|
};
|
|
if is_nullish(&value) {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
"missing pubKeyCredParams".to_owned(),
|
|
));
|
|
}
|
|
|
|
let raw = value.raw();
|
|
let raw_env = value.value().env;
|
|
let mut is_array = false;
|
|
let status = unsafe { napi::sys::napi_is_array(raw_env, raw, &mut is_array) };
|
|
if status != napi::sys::Status::napi_ok || !is_array {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
"pubKeyCredParams must be an array".to_owned(),
|
|
));
|
|
}
|
|
let arr: Object = unsafe { value.cast::<Object>()? };
|
|
let len = read_array_length(&arr)?;
|
|
if len == 0 {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
"pubKeyCredParams must not be empty".to_owned(),
|
|
));
|
|
}
|
|
let mut algs = Vec::with_capacity(len as usize);
|
|
for i in 0..len {
|
|
let mut elem_raw: napi::sys::napi_value = std::ptr::null_mut();
|
|
let status = unsafe { napi::sys::napi_get_element(raw_env, raw, i, &mut elem_raw) };
|
|
if status != napi::sys::Status::napi_ok {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
"pubKeyCredParams element read failed".to_owned(),
|
|
));
|
|
}
|
|
|
|
let elem_value: Unknown = unsafe { Unknown::from_raw_unchecked(raw_env, elem_raw) };
|
|
if elem_value.get_type()? != ValueType::Object {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
"pubKeyCredParams[] must be objects".to_owned(),
|
|
));
|
|
}
|
|
let elem: Object = unsafe { elem_value.cast::<Object>()? };
|
|
algs.push(optional_i32_field(&elem, "alg", 0)?);
|
|
}
|
|
Ok(algs)
|
|
}
|
|
|
|
fn optional_i32_field(object: &Object, key: &str, default: i32) -> Result<i32> {
|
|
let Some(value) = read_unknown(object, key) else {
|
|
return Ok(default);
|
|
};
|
|
if is_nullish(&value) {
|
|
return Ok(default);
|
|
}
|
|
if value.get_type()? != ValueType::Number {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} must be a number"),
|
|
));
|
|
}
|
|
let n: i32 = unsafe { value.cast::<i32>()? };
|
|
Ok(n)
|
|
}
|
|
|
|
fn optional_descriptor_array(object: &Object, key: &str) -> Result<Vec<DescriptorInput>> {
|
|
let Some(value) = read_unknown(object, key) else {
|
|
return Ok(Vec::new());
|
|
};
|
|
if is_nullish(&value) {
|
|
return Ok(Vec::new());
|
|
}
|
|
let raw = value.raw();
|
|
let raw_env = value.value().env;
|
|
let mut is_array = false;
|
|
let status = unsafe { napi::sys::napi_is_array(raw_env, raw, &mut is_array) };
|
|
if status != napi::sys::Status::napi_ok || !is_array {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key} must be an array"),
|
|
));
|
|
}
|
|
let arr: Object = unsafe { value.cast::<Object>()? };
|
|
let len = read_array_length(&arr)?;
|
|
let mut out = Vec::with_capacity(len as usize);
|
|
for i in 0..len {
|
|
let mut elem_raw: napi::sys::napi_value = std::ptr::null_mut();
|
|
let status = unsafe { napi::sys::napi_get_element(raw_env, raw, i, &mut elem_raw) };
|
|
if status != napi::sys::Status::napi_ok {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key}[] read failed"),
|
|
));
|
|
}
|
|
let elem_value: Unknown = unsafe { Unknown::from_raw_unchecked(raw_env, elem_raw) };
|
|
if elem_value.get_type()? != ValueType::Object {
|
|
return Err(Error::new(
|
|
Status::InvalidArg,
|
|
format!("{key}[] must be objects"),
|
|
));
|
|
}
|
|
let elem: Object = unsafe { elem_value.cast::<Object>()? };
|
|
let id = required_buffer(&elem, "id")?;
|
|
let transports = optional_u32(&elem, "transports", 0)?;
|
|
out.push(DescriptorInput { id, transports });
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn read_array_length(obj: &Object) -> Result<u32> {
|
|
let length: u32 = obj.get_named_property::<u32>("length")?;
|
|
Ok(length)
|
|
}
|