Add native self-hosted instance connection to fluxer_desktop
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.
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
#![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)
|
||||
}
|
||||
Reference in New Issue
Block a user