mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Add experimental user verification API contracts (#43265)
## What changed Add `userVerification/status`, `userVerification/enroll`, `userVerification/delete`, and `userVerification/verify` behind the `experimentalApi` capability. Define request, response, proof, and typed error contracts, export their schemas, and document the API. All four methods currently return `unavailable` with reason `providerUnavailable`; native verification operations are not implemented by this change. ## Testing Add tests for experimental opt-in, typed provider unavailability, readiness serialization, and rejection of mismatched error reasons and native diagnostic payloads. GitOrigin-RevId: a858ab41523c5acec7dbf3cfa7fcab693ac44a8c
This commit is contained in:
1 parent
7bab4526f9
commit
ad931a45b2
12 files changed
+368
No files matched your search
BIN
Binary file not shown.
@@ -55,6 +55,13 @@ const EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES: &[&str] = &[
|
|||||||
"ThreadSearchOccurrence",
|
"ThreadSearchOccurrence",
|
||||||
"ThreadSearchTextRange",
|
"ThreadSearchTextRange",
|
||||||
"TurnSettingsUpdateStatus",
|
"TurnSettingsUpdateStatus",
|
||||||
|
"UserVerificationProof",
|
||||||
|
"UserVerificationCancellationReason",
|
||||||
|
"UserVerificationErrorDetails",
|
||||||
|
"UserVerificationFailureReason",
|
||||||
|
"UserVerificationInvalidRequestReason",
|
||||||
|
"UserVerificationRpcError",
|
||||||
|
"UserVerificationUnavailableReason",
|
||||||
];
|
];
|
||||||
const SPECIAL_DEFINITIONS: &[&str] = &[
|
const SPECIAL_DEFINITIONS: &[&str] = &[
|
||||||
"ClientNotification",
|
"ClientNotification",
|
||||||
@@ -123,6 +130,7 @@ pub fn generate_ts_with_options(
|
|||||||
ClientRequest::export_all_to(out_dir)?;
|
ClientRequest::export_all_to(out_dir)?;
|
||||||
export_client_responses(out_dir)?;
|
export_client_responses(out_dir)?;
|
||||||
ClientNotification::export_all_to(out_dir)?;
|
ClientNotification::export_all_to(out_dir)?;
|
||||||
|
crate::UserVerificationRpcError::export_all_to(out_dir)?;
|
||||||
|
|
||||||
ServerRequest::export_all_to(out_dir)?;
|
ServerRequest::export_all_to(out_dir)?;
|
||||||
export_server_responses(out_dir)?;
|
export_server_responses(out_dir)?;
|
||||||
@@ -220,6 +228,10 @@ pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) -
|
|||||||
schemas.push(emit(out_dir)?);
|
schemas.push(emit(out_dir)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schemas.push(write_json_schema::<crate::UserVerificationRpcError>(
|
||||||
|
out_dir,
|
||||||
|
"v2::UserVerificationRpcError",
|
||||||
|
)?);
|
||||||
schemas.extend(export_client_param_schemas(out_dir)?);
|
schemas.extend(export_client_param_schemas(out_dir)?);
|
||||||
schemas.extend(export_client_response_schemas(out_dir)?);
|
schemas.extend(export_client_response_schemas(out_dir)?);
|
||||||
schemas.extend(export_server_param_schemas(out_dir)?);
|
schemas.extend(export_server_param_schemas(out_dir)?);
|
||||||
|
|||||||
@@ -518,6 +518,34 @@ client_request_definitions! {
|
|||||||
response: v2::ServerDiagnosticsResponse,
|
response: v2::ServerDiagnosticsResponse,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[experimental("userVerification/status")]
|
||||||
|
UserVerificationStatus => "userVerification/status" {
|
||||||
|
params: v2::UserVerificationStatusParams,
|
||||||
|
serialization: None,
|
||||||
|
response: v2::UserVerificationStatusResponse,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[experimental("userVerification/enroll")]
|
||||||
|
UserVerificationEnroll => "userVerification/enroll" {
|
||||||
|
params: v2::UserVerificationEnrollParams,
|
||||||
|
serialization: None,
|
||||||
|
response: v2::UserVerificationEnrollResponse,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[experimental("userVerification/delete")]
|
||||||
|
UserVerificationDelete => "userVerification/delete" {
|
||||||
|
params: v2::UserVerificationDeleteParams,
|
||||||
|
serialization: None,
|
||||||
|
response: v2::UserVerificationDeleteResponse,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[experimental("userVerification/verify")]
|
||||||
|
UserVerificationVerify => "userVerification/verify" {
|
||||||
|
params: v2::UserVerificationVerifyParams,
|
||||||
|
serialization: None,
|
||||||
|
response: v2::UserVerificationVerifyResponse,
|
||||||
|
},
|
||||||
|
|
||||||
/// NEW APIs
|
/// NEW APIs
|
||||||
// Thread lifecycle
|
// Thread lifecycle
|
||||||
// Uses `inspect_params` because only some fields are experimental.
|
// Uses `inspect_params` because only some fields are experimental.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ mod thread;
|
|||||||
mod thread_data;
|
mod thread_data;
|
||||||
mod thread_usage;
|
mod thread_usage;
|
||||||
mod turn;
|
mod turn;
|
||||||
|
mod user_verification;
|
||||||
mod windows_sandbox;
|
mod windows_sandbox;
|
||||||
|
|
||||||
pub use account::*;
|
pub use account::*;
|
||||||
@@ -69,6 +70,7 @@ pub use thread::*;
|
|||||||
pub use thread_data::*;
|
pub use thread_data::*;
|
||||||
pub use thread_usage::*;
|
pub use thread_usage::*;
|
||||||
pub use turn::*;
|
pub use turn::*;
|
||||||
|
pub use user_verification::*;
|
||||||
pub use windows_sandbox::*;
|
pub use windows_sandbox::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
//! Experimental user-verification APIs for trusted UI clients.
|
||||||
|
//! Native implementation and registration transport are independent of these contracts.
|
||||||
|
|
||||||
|
use crate::JsonSchema;
|
||||||
|
use crate::TS;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// A signature over the exact decoded challenge. The verifier validates and consumes it.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationProof {
|
||||||
|
pub credential_id: String,
|
||||||
|
/// Unpadded base64url DER ECDSA signature using P-256 and SHA-256.
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for UserVerificationProof {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("UserVerificationProof")
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||||
|
pub enum UserVerificationUnavailableReason {
|
||||||
|
CredentialMissing,
|
||||||
|
BiometricsUnavailable,
|
||||||
|
ProviderUnavailable,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||||
|
pub enum UserVerificationCancellationReason {
|
||||||
|
UserCancelled,
|
||||||
|
Interrupted,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||||
|
pub enum UserVerificationFailureReason {
|
||||||
|
AuthenticationFailed,
|
||||||
|
Timeout,
|
||||||
|
ProviderError,
|
||||||
|
ServiceError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||||
|
pub enum UserVerificationInvalidRequestReason {
|
||||||
|
InvalidParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closed error categories; native diagnostic payloads must not cross this boundary.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(tag = "type", rename_all = "camelCase", export_to = "v2/")]
|
||||||
|
pub enum UserVerificationErrorDetails {
|
||||||
|
InvalidRequest {
|
||||||
|
reason: UserVerificationInvalidRequestReason,
|
||||||
|
},
|
||||||
|
Unavailable {
|
||||||
|
reason: UserVerificationUnavailableReason,
|
||||||
|
},
|
||||||
|
Cancelled {
|
||||||
|
reason: UserVerificationCancellationReason,
|
||||||
|
},
|
||||||
|
Failed {
|
||||||
|
reason: UserVerificationFailureReason,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error object inside the normal JSON-RPC envelope.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationRpcError {
|
||||||
|
#[ts(type = "number")]
|
||||||
|
pub code: i64,
|
||||||
|
pub message: String,
|
||||||
|
pub data: UserVerificationErrorDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationStatusParams {}
|
||||||
|
|
||||||
|
/// Local readiness only; this neither prompts nor queries server registration.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationStatusResponse {
|
||||||
|
pub credential_id: Option<String>,
|
||||||
|
pub unavailable_reason: Option<UserVerificationUnavailableReason>,
|
||||||
|
pub unavailable_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationEnrollParams {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationEnrollResponse {
|
||||||
|
pub credential_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationDeleteParams {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationDeleteResponse {}
|
||||||
|
|
||||||
|
/// Local signing primitive, independent of any pending elicitation.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationVerifyParams {
|
||||||
|
/// Unpadded base64url encoding of 1–4096 challenge bytes.
|
||||||
|
pub challenge: String,
|
||||||
|
/// Display context already approved by the UI; 1–256 UTF-8 bytes.
|
||||||
|
pub title: String,
|
||||||
|
/// Additional display context; at most 4096 UTF-8 bytes.
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct UserVerificationVerifyResponse {
|
||||||
|
pub proof: UserVerificationProof,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "user_verification_tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_readiness_retains_credential_during_biometric_unavailability() {
|
||||||
|
let status = UserVerificationStatusResponse {
|
||||||
|
credential_id: Some("credential".into()),
|
||||||
|
unavailable_reason: Some(UserVerificationUnavailableReason::BiometricsUnavailable),
|
||||||
|
unavailable_message: Some("Unlock biometrics to continue.".into()),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&status).unwrap(),
|
||||||
|
json!({
|
||||||
|
"credentialId": "credential",
|
||||||
|
"unavailableReason": "biometricsUnavailable",
|
||||||
|
"unavailableMessage": "Unlock biometrics to continue."
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_value::<UserVerificationStatusResponse>(
|
||||||
|
serde_json::to_value(&status).unwrap()
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_details_reject_cross_category_reasons_and_native_payloads() {
|
||||||
|
let details = UserVerificationErrorDetails::Cancelled {
|
||||||
|
reason: UserVerificationCancellationReason::UserCancelled,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(details).unwrap(),
|
||||||
|
json!({"type": "cancelled", "reason": "userCancelled"})
|
||||||
|
);
|
||||||
|
for invalid in [
|
||||||
|
json!({"type": "failed", "reason": "userCancelled"}),
|
||||||
|
json!({"type": "unavailable", "reason": "providerUnavailable", "nativeError": {"code": 1}}),
|
||||||
|
] {
|
||||||
|
assert!(serde_json::from_value::<UserVerificationErrorDetails>(invalid).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3007,3 +3007,33 @@ For server-initiated request payloads, annotate the field the same way so schema
|
|||||||
```bash
|
```bash
|
||||||
just test -p codex-app-server-protocol
|
just test -p codex-app-server-protocol
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## User verification (experimental)
|
||||||
|
|
||||||
|
Trusted UI clients use four experimental methods. They require the existing
|
||||||
|
`experimentalApi` opt-in. This contract-only stage returns
|
||||||
|
`unavailable/providerUnavailable` until native operations are connected.
|
||||||
|
|
||||||
|
| Method | Params | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `userVerification/status` | `{}` | `{credentialId, unavailableReason, unavailableMessage}` |
|
||||||
|
| `userVerification/enroll` | `{}` | `{credentialId}` |
|
||||||
|
| `userVerification/delete` | `{}` | `{}` |
|
||||||
|
| `userVerification/verify` | `{challenge, title, description}` | `{proof: {credentialId, signature}}` |
|
||||||
|
|
||||||
|
Status reads local readiness without prompting or contacting a backend. A null
|
||||||
|
`unavailableReason` means local checks passed, not that registration is valid.
|
||||||
|
Enrollment and deletion coordinate credential lifecycle; callers do not issue
|
||||||
|
separate generate or rotate commands. Identity comes from the authenticated
|
||||||
|
account; this API exposes no caller-selected scope.
|
||||||
|
|
||||||
|
Verify signs 1–4096 decoded challenge bytes using P-256 ECDSA with SHA-256. The
|
||||||
|
challenge and DER signature use unpadded base64url. Title is 1–256 UTF-8 bytes;
|
||||||
|
description is at most 4096 bytes. The UI obtains approval for that display
|
||||||
|
context before calling. Verify does not require a pending elicitation; a UI with
|
||||||
|
its own authenticator can return proof directly in elicitation response content.
|
||||||
|
The calling flow owns pending-request checks and discards late proofs.
|
||||||
|
|
||||||
|
Failures use the normal JSON-RPC error envelope with closed `{type, reason}` data:
|
||||||
|
`invalidRequest`, `unavailable`, `cancelled`, or `failed`. UI clients branch on
|
||||||
|
these values rather than message text. Native diagnostic payloads stay private.
|
||||||
@@ -132,6 +132,7 @@ mod thread_state;
|
|||||||
mod thread_status;
|
mod thread_status;
|
||||||
mod transport;
|
mod transport;
|
||||||
mod turn_cost_worker;
|
mod turn_cost_worker;
|
||||||
|
mod user_verification;
|
||||||
|
|
||||||
pub use crate::code_mode_host::AppServerCodeModeHostArgs;
|
pub use crate::code_mode_host::AppServerCodeModeHostArgs;
|
||||||
pub use crate::code_mode_host::CodeModeHostTransport;
|
pub use crate::code_mode_host::CodeModeHostTransport;
|
||||||
|
|||||||
@@ -980,6 +980,12 @@ impl MessageProcessor {
|
|||||||
ClientRequest::Initialize { .. } => {
|
ClientRequest::Initialize { .. } => {
|
||||||
panic!("Initialize should be handled before initialized request dispatch");
|
panic!("Initialize should be handled before initialized request dispatch");
|
||||||
}
|
}
|
||||||
|
ClientRequest::UserVerificationStatus { .. }
|
||||||
|
| ClientRequest::UserVerificationEnroll { .. }
|
||||||
|
| ClientRequest::UserVerificationDelete { .. }
|
||||||
|
| ClientRequest::UserVerificationVerify { .. } => {
|
||||||
|
Err(crate::user_verification::unavailable())
|
||||||
|
}
|
||||||
ClientRequest::ServerDiagnostics { .. } => Ok(Some(read_server_diagnostics().into())),
|
ClientRequest::ServerDiagnostics { .. } => Ok(Some(read_server_diagnostics().into())),
|
||||||
ClientRequest::ConfigRead { params, .. } => self
|
ClientRequest::ConfigRead { params, .. } => self
|
||||||
.config_processor
|
.config_processor
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! Dispatch boundary for experimental verification APIs.
|
||||||
|
//! Native operations are introduced separately from the public contract.
|
||||||
|
|
||||||
|
use codex_app_server_protocol::JSONRPCErrorError;
|
||||||
|
use codex_app_server_protocol::UserVerificationErrorDetails;
|
||||||
|
use codex_app_server_protocol::UserVerificationUnavailableReason;
|
||||||
|
|
||||||
|
pub(crate) fn unavailable() -> JSONRPCErrorError {
|
||||||
|
JSONRPCErrorError {
|
||||||
|
code: -32603,
|
||||||
|
message: "User verification is not available in this build.".into(),
|
||||||
|
data: serde_json::to_value(UserVerificationErrorDetails::Unavailable {
|
||||||
|
reason: UserVerificationUnavailableReason::ProviderUnavailable,
|
||||||
|
})
|
||||||
|
.ok(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,3 +130,5 @@ mod turn_steer;
|
|||||||
mod view_image;
|
mod view_image;
|
||||||
mod web_search;
|
mod web_search;
|
||||||
mod windows_sandbox_setup;
|
mod windows_sandbox_setup;
|
||||||
|
|
||||||
|
mod user_verification;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use app_test_support::DEFAULT_CLIENT_NAME;
|
||||||
|
use app_test_support::TestAppServer;
|
||||||
|
use codex_app_server_protocol::ClientInfo;
|
||||||
|
use codex_app_server_protocol::InitializeCapabilities;
|
||||||
|
use codex_app_server_protocol::JSONRPCErrorError;
|
||||||
|
use codex_app_server_protocol::RequestId;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_verification_methods_require_experimental_opt_in() -> Result<()> {
|
||||||
|
let mut app_server = TestAppServer::builder().build().await?;
|
||||||
|
app_server
|
||||||
|
.initialize_with_capabilities(
|
||||||
|
ClientInfo {
|
||||||
|
name: DEFAULT_CLIENT_NAME.into(),
|
||||||
|
title: None,
|
||||||
|
version: "0.1.0".into(),
|
||||||
|
},
|
||||||
|
Some(InitializeCapabilities {
|
||||||
|
experimental_api: false,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for (method, params) in [
|
||||||
|
("userVerification/status", json!({})),
|
||||||
|
("userVerification/enroll", json!({})),
|
||||||
|
("userVerification/delete", json!({})),
|
||||||
|
(
|
||||||
|
"userVerification/verify",
|
||||||
|
json!({"challenge": "AQ", "title": "Approve", "description": ""}),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let id = app_server.send_raw_request(method, Some(params)).await?;
|
||||||
|
let response = app_server
|
||||||
|
.read_stream_until_error_message(RequestId::Integer(id))
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
response.error,
|
||||||
|
JSONRPCErrorError {
|
||||||
|
code: -32600,
|
||||||
|
message: format!("{method} requires experimentalApi capability"),
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_verification_without_provider_returns_typed_unavailability() -> Result<()> {
|
||||||
|
let mut app_server = TestAppServer::builder().build_initialized().await?;
|
||||||
|
let id = app_server
|
||||||
|
.send_raw_request(
|
||||||
|
"userVerification/verify",
|
||||||
|
Some(json!({
|
||||||
|
"challenge": "AQ", "title": "Approve", "description": ""
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let response = app_server
|
||||||
|
.read_stream_until_error_message(RequestId::Integer(id))
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
response.error,
|
||||||
|
JSONRPCErrorError {
|
||||||
|
code: -32603,
|
||||||
|
message: "User verification is not available in this build.".into(),
|
||||||
|
data: Some(json!({"type": "unavailable", "reason": "providerUnavailable"})),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user