#[cfg(feature = "openapi-code-mode")]
use crate::config::OperationRegistry;
use crate::graphql::GraphQLQueryInfo;
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct ServerConfigEntity {
pub server_id: String,
pub server_type: String,
pub allow_write: bool,
pub allow_delete: bool,
pub allow_admin: bool,
pub allowed_operations: HashSet<String>,
pub blocked_operations: HashSet<String>,
pub max_depth: u32,
pub max_field_count: u32,
pub max_cost: u32,
pub max_api_calls: u32,
pub blocked_fields: HashSet<String>,
pub allowed_sensitive_categories: HashSet<String>,
}
impl Default for ServerConfigEntity {
fn default() -> Self {
Self {
server_id: "unknown".to_string(),
server_type: "graphql".to_string(),
allow_write: false,
allow_delete: false,
allow_admin: false,
allowed_operations: HashSet::new(),
blocked_operations: HashSet::new(),
max_depth: 10,
max_field_count: 100,
max_cost: 1000,
max_api_calls: 50,
blocked_fields: HashSet::new(),
allowed_sensitive_categories: HashSet::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct OperationEntity {
pub id: String,
pub operation_type: String,
pub operation_name: String,
pub root_fields: HashSet<String>,
pub accessed_types: HashSet<String>,
pub accessed_fields: HashSet<String>,
pub depth: u32,
pub field_count: u32,
pub estimated_cost: u32,
pub has_introspection: bool,
pub accesses_sensitive_data: bool,
pub sensitive_categories: HashSet<String>,
}
impl OperationEntity {
pub fn from_query_info(query_info: &GraphQLQueryInfo) -> Self {
use crate::graphql::GraphQLOperationType;
let operation_type = match query_info.operation_type {
GraphQLOperationType::Query => "query",
GraphQLOperationType::Mutation => "mutation",
GraphQLOperationType::Subscription => "subscription",
};
Self {
id: query_info
.operation_name
.clone()
.unwrap_or_else(|| "anonymous".to_string()),
operation_type: operation_type.to_string(),
operation_name: query_info.operation_name.clone().unwrap_or_default(),
root_fields: query_info.root_fields.iter().cloned().collect(),
accessed_types: query_info.types_accessed.iter().cloned().collect(),
accessed_fields: query_info.fields_accessed.iter().cloned().collect(),
depth: query_info.max_depth as u32,
field_count: query_info.fields_accessed.len() as u32,
estimated_cost: query_info.fields_accessed.len() as u32,
has_introspection: query_info.has_introspection,
accesses_sensitive_data: false,
sensitive_categories: HashSet::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationDecision {
pub allowed: bool,
pub determining_policies: Vec<String>,
pub errors: Vec<String>,
}
#[cfg(feature = "openapi-code-mode")]
#[derive(Debug, Clone)]
pub struct ScriptEntity {
pub id: String,
pub script_type: String,
pub has_writes: bool,
pub has_deletes: bool,
pub total_api_calls: u32,
pub read_calls: u32,
pub write_calls: u32,
pub delete_calls: u32,
pub accessed_paths: HashSet<String>,
pub accessed_methods: HashSet<String>,
pub path_patterns: HashSet<String>,
pub called_operations: HashSet<String>,
pub loop_iterations: u32,
pub nesting_depth: u32,
pub script_length: u32,
pub accesses_sensitive_path: bool,
pub has_unbounded_loop: bool,
pub has_dynamic_path: bool,
pub has_output_declaration: bool,
pub output_fields: HashSet<String>,
pub has_spread_in_output: bool,
}
#[cfg(feature = "openapi-code-mode")]
impl ScriptEntity {
pub fn from_javascript_info(
info: &crate::javascript::JavaScriptCodeInfo,
sensitive_patterns: &[String],
registry: Option<&OperationRegistry>,
) -> Self {
use crate::javascript::HttpMethod;
let mut accessed_paths = HashSet::new();
let mut accessed_methods = HashSet::new();
let mut path_patterns = HashSet::new();
let mut called_operations = HashSet::new();
let mut read_calls = 0u32;
let mut write_calls = 0u32;
let mut delete_calls = 0u32;
let mut has_dynamic_path = false;
let mut accesses_sensitive_path = false;
for api_call in &info.api_calls {
accessed_paths.insert(api_call.path.clone());
let method_str = format!("{:?}", api_call.method).to_uppercase();
accessed_methods.insert(method_str.clone());
let pattern = normalize_path_to_pattern(&api_call.path);
path_patterns.insert(pattern.clone());
let op_id = registry
.and_then(|r| r.lookup(&api_call.path))
.map(|id| id.to_string())
.unwrap_or_else(|| format!("{}:{}", method_str, pattern));
called_operations.insert(op_id);
let call_category = registry.and_then(|r| r.lookup_category(&api_call.path));
match call_category {
Some("read") => read_calls += 1,
Some("delete") => delete_calls += 1,
Some("write" | "admin") => write_calls += 1,
Some(_) => write_calls += 1,
None => match api_call.method {
HttpMethod::Get | HttpMethod::Head | HttpMethod::Options => read_calls += 1,
HttpMethod::Delete => delete_calls += 1,
_ => write_calls += 1,
},
}
if api_call.is_dynamic_path {
has_dynamic_path = true;
}
let path_lower = api_call.path.to_lowercase();
for pattern in sensitive_patterns {
if path_lower.contains(&pattern.to_lowercase()) {
accesses_sensitive_path = true;
break;
}
}
}
let has_writes = write_calls > 0 || delete_calls > 0;
let has_reads = read_calls > 0;
let script_type = match (has_reads, has_writes) {
(true, false) => "read_only",
(false, true) => "write_only",
(true, true) => "mixed",
(false, false) => "empty",
};
Self {
id: info
.api_calls
.first()
.map(|c| format!("{}:{}", format!("{:?}", c.method).to_uppercase(), c.path))
.unwrap_or_else(|| "script".to_string()),
script_type: script_type.to_string(),
has_writes,
has_deletes: delete_calls > 0,
total_api_calls: info.api_calls.len() as u32,
read_calls,
write_calls,
delete_calls,
accessed_paths,
accessed_methods,
path_patterns,
called_operations,
loop_iterations: 0,
nesting_depth: info.max_depth as u32,
script_length: 0,
accesses_sensitive_path,
has_unbounded_loop: !info.all_loops_bounded && info.loop_count > 0,
has_dynamic_path,
has_output_declaration: info.output_declaration.has_declaration,
output_fields: info.output_declaration.declared_fields.clone(),
has_spread_in_output: info.output_declaration.has_spread_risk
|| info.has_output_spread_risk,
}
}
pub fn action(&self) -> &'static str {
match self.script_type.as_str() {
"read_only" | "empty" => "Read",
"write_only" | "mixed" => {
if self.has_deletes {
"Delete"
} else {
"Write"
}
},
_ => "Read",
}
}
}
#[cfg(feature = "openapi-code-mode")]
fn is_uuid_like(segment: &str) -> bool {
if segment.len() != 36 {
return false;
}
let parts: Vec<&str> = segment.split('-').collect();
matches!(parts.as_slice(), [a, b, c, d, e]
if a.len() == 8 && b.len() == 4 && c.len() == 4
&& d.len() == 4 && e.len() == 12
&& segment.chars().all(|ch| ch.is_ascii_hexdigit() || ch == '-'))
}
#[cfg(feature = "openapi-code-mode")]
pub fn normalize_path_to_pattern(path: &str) -> String {
path.split('/')
.map(|segment| {
if segment.chars().all(|c| c.is_ascii_digit()) || is_uuid_like(segment) {
"*"
} else {
segment
}
})
.collect::<Vec<_>>()
.join("/")
}
#[cfg(feature = "openapi-code-mode")]
pub fn normalize_operation_format(op: &str) -> String {
let trimmed = op.trim();
let (method, path) = if let Some(idx) = trimmed.find(':') {
let potential_path = trimmed[idx + 1..].trim();
if potential_path.starts_with('/') {
let method = trimmed[..idx].trim();
(method, potential_path)
} else {
return trimmed.to_string();
}
} else if let Some(idx) = trimmed.find(' ') {
let method = trimmed[..idx].trim();
let path = trimmed[idx + 1..].trim();
(method, path)
} else {
return trimmed.to_string();
};
let method_upper = method.to_uppercase();
let normalized_path = path
.split('/')
.map(|segment| {
if segment.starts_with('{') && segment.ends_with('}') {
"*"
} else if segment.starts_with(':') {
"*"
} else if segment.chars().all(|c| c.is_ascii_digit()) {
"*"
} else if is_uuid_like(segment) {
"*"
} else {
segment
}
})
.collect::<Vec<_>>()
.join("/");
format!("{}:{}", method_upper, normalized_path)
}
#[cfg(feature = "openapi-code-mode")]
#[derive(Debug, Clone)]
pub struct OpenAPIServerEntity {
pub server_id: String,
pub server_type: String,
pub allow_write: bool,
pub allow_delete: bool,
pub allow_admin: bool,
pub write_mode: String,
pub max_depth: u32,
pub max_cost: u32,
pub max_api_calls: u32,
pub max_loop_iterations: u32,
pub max_script_length: u32,
pub max_nesting_depth: u32,
pub execution_timeout_seconds: u32,
pub allowed_operations: HashSet<String>,
pub blocked_operations: HashSet<String>,
pub allowed_methods: HashSet<String>,
pub blocked_methods: HashSet<String>,
pub allowed_path_patterns: HashSet<String>,
pub blocked_path_patterns: HashSet<String>,
pub sensitive_path_patterns: HashSet<String>,
pub auto_approve_read_only: bool,
pub max_api_calls_for_auto_approve: u32,
pub internal_blocked_fields: HashSet<String>,
pub output_blocked_fields: HashSet<String>,
pub require_output_declaration: bool,
}
#[cfg(feature = "openapi-code-mode")]
impl Default for OpenAPIServerEntity {
fn default() -> Self {
Self {
server_id: "unknown".to_string(),
server_type: "openapi".to_string(),
allow_write: false,
allow_delete: false,
allow_admin: false,
write_mode: "deny_all".to_string(),
max_depth: 10,
max_cost: 1000,
max_api_calls: 50,
max_loop_iterations: 100,
max_script_length: 10000,
max_nesting_depth: 10,
execution_timeout_seconds: 30,
allowed_operations: HashSet::new(),
blocked_operations: HashSet::new(),
allowed_methods: HashSet::new(),
blocked_methods: HashSet::new(),
allowed_path_patterns: HashSet::new(),
blocked_path_patterns: ["/admin".into(), "/internal".into()].into_iter().collect(),
sensitive_path_patterns: ["/admin".into(), "/internal".into(), "/debug".into()]
.into_iter()
.collect(),
auto_approve_read_only: true,
max_api_calls_for_auto_approve: 10,
internal_blocked_fields: HashSet::new(),
output_blocked_fields: HashSet::new(),
require_output_declaration: false,
}
}
}
pub fn get_code_mode_schema_json() -> serde_json::Value {
let applies_to = serde_json::json!({
"principalTypes": ["Operation"],
"resourceTypes": ["Server"],
"context": {
"type": "Record",
"attributes": {
"serverId": { "type": "String", "required": true },
"serverType": { "type": "String", "required": true },
"userId": { "type": "String", "required": false },
"sessionId": { "type": "String", "required": false }
}
}
});
serde_json::json!({
"CodeMode": {
"entityTypes": {
"Operation": {
"shape": {
"type": "Record",
"attributes": {
"operationType": { "type": "String", "required": true },
"operationName": { "type": "String", "required": true },
"rootFields": { "type": "Set", "element": { "type": "String" } },
"accessedTypes": { "type": "Set", "element": { "type": "String" } },
"accessedFields": { "type": "Set", "element": { "type": "String" } },
"depth": { "type": "Long", "required": true },
"fieldCount": { "type": "Long", "required": true },
"estimatedCost": { "type": "Long", "required": true },
"hasIntrospection": { "type": "Boolean", "required": true },
"accessesSensitiveData": { "type": "Boolean", "required": true },
"sensitiveCategories": { "type": "Set", "element": { "type": "String" } }
}
}
},
"Server": {
"shape": {
"type": "Record",
"attributes": {
"serverId": { "type": "String", "required": true },
"serverType": { "type": "String", "required": true },
"maxDepth": { "type": "Long", "required": true },
"maxFieldCount": { "type": "Long", "required": true },
"maxCost": { "type": "Long", "required": true },
"maxApiCalls": { "type": "Long", "required": true },
"allowWrite": { "type": "Boolean", "required": true },
"allowDelete": { "type": "Boolean", "required": true },
"allowAdmin": { "type": "Boolean", "required": true },
"blockedOperations": { "type": "Set", "element": { "type": "String" } },
"allowedOperations": { "type": "Set", "element": { "type": "String" } },
"blockedFields": { "type": "Set", "element": { "type": "String" } }
}
}
}
},
"actions": {
"Read": { "appliesTo": applies_to },
"Write": { "appliesTo": applies_to },
"Delete": { "appliesTo": applies_to },
"Admin": { "appliesTo": applies_to }
}
}
})
}
#[cfg(feature = "openapi-code-mode")]
pub fn get_openapi_code_mode_schema_json() -> serde_json::Value {
let applies_to = serde_json::json!({
"principalTypes": ["Script"],
"resourceTypes": ["Server"],
"context": {
"type": "Record",
"attributes": {
"serverId": { "type": "String", "required": true },
"serverType": { "type": "String", "required": true },
"userId": { "type": "String", "required": false },
"sessionId": { "type": "String", "required": false }
}
}
});
serde_json::json!({
"CodeMode": {
"entityTypes": {
"Script": {
"shape": {
"type": "Record",
"attributes": {
"scriptType": { "type": "String", "required": true },
"hasWrites": { "type": "Boolean", "required": true },
"hasDeletes": { "type": "Boolean", "required": true },
"totalApiCalls": { "type": "Long", "required": true },
"readCalls": { "type": "Long", "required": true },
"writeCalls": { "type": "Long", "required": true },
"deleteCalls": { "type": "Long", "required": true },
"accessedPaths": { "type": "Set", "element": { "type": "String" } },
"accessedMethods": { "type": "Set", "element": { "type": "String" } },
"pathPatterns": { "type": "Set", "element": { "type": "String" } },
"calledOperations": { "type": "Set", "element": { "type": "String" } },
"loopIterations": { "type": "Long", "required": true },
"nestingDepth": { "type": "Long", "required": true },
"scriptLength": { "type": "Long", "required": true },
"accessesSensitivePath": { "type": "Boolean", "required": true },
"hasUnboundedLoop": { "type": "Boolean", "required": true },
"hasDynamicPath": { "type": "Boolean", "required": true },
"outputFields": { "type": "Set", "element": { "type": "String" } },
"hasOutputDeclaration": { "type": "Boolean", "required": true },
"hasSpreadInOutput": { "type": "Boolean", "required": true }
}
}
},
"Server": {
"shape": {
"type": "Record",
"attributes": {
"serverId": { "type": "String", "required": true },
"serverType": { "type": "String", "required": true },
"writeMode": { "type": "String", "required": true },
"maxDepth": { "type": "Long", "required": true },
"maxCost": { "type": "Long", "required": true },
"maxApiCalls": { "type": "Long", "required": true },
"allowWrite": { "type": "Boolean", "required": true },
"allowDelete": { "type": "Boolean", "required": true },
"allowAdmin": { "type": "Boolean", "required": true },
"blockedOperations": { "type": "Set", "element": { "type": "String" } },
"allowedOperations": { "type": "Set", "element": { "type": "String" } },
"blockedFields": { "type": "Set", "element": { "type": "String" } },
"maxLoopIterations": { "type": "Long", "required": true },
"maxScriptLength": { "type": "Long", "required": true },
"maxNestingDepth": { "type": "Long", "required": true },
"executionTimeoutSeconds": { "type": "Long", "required": true },
"allowedMethods": { "type": "Set", "element": { "type": "String" } },
"blockedMethods": { "type": "Set", "element": { "type": "String" } },
"allowedPathPatterns": { "type": "Set", "element": { "type": "String" } },
"blockedPathPatterns": { "type": "Set", "element": { "type": "String" } },
"sensitivePathPatterns": { "type": "Set", "element": { "type": "String" } },
"autoApproveReadOnly": { "type": "Boolean", "required": true },
"maxApiCallsForAutoApprove": { "type": "Long", "required": true },
"internalBlockedFields": { "type": "Set", "element": { "type": "String" } },
"outputBlockedFields": { "type": "Set", "element": { "type": "String" } },
"requireOutputDeclaration": { "type": "Boolean", "required": true }
}
}
}
},
"actions": {
"Read": { "appliesTo": applies_to },
"Write": { "appliesTo": applies_to },
"Delete": { "appliesTo": applies_to },
"Admin": { "appliesTo": applies_to }
}
}
})
}
#[cfg(feature = "openapi-code-mode")]
pub fn get_openapi_baseline_policies() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
(
"permit_reads",
"Permit all read operations (GET scripts)",
r#"permit(principal, action == CodeMode::Action::"Read", resource);"#,
),
(
"permit_writes",
"Permit write operations (when enabled)",
r#"permit(principal, action == CodeMode::Action::"Write", resource) when { resource.allowWrite == true };"#,
),
(
"permit_deletes",
"Permit delete operations (when enabled)",
r#"permit(principal, action == CodeMode::Action::"Delete", resource) when { resource.allowDelete == true };"#,
),
(
"forbid_sensitive_paths",
"Block scripts accessing sensitive paths",
r#"forbid(principal, action, resource) when { principal.accessesSensitivePath == true };"#,
),
(
"forbid_unbounded_loops",
"Block scripts with unbounded loops",
r#"forbid(principal, action, resource) when { principal.hasUnboundedLoop == true };"#,
),
(
"forbid_excessive_api_calls",
"Enforce API call limit",
r#"forbid(principal, action, resource) when { principal.totalApiCalls > resource.maxApiCalls };"#,
),
(
"forbid_excessive_nesting",
"Enforce nesting depth limit",
r#"forbid(principal, action, resource) when { principal.nestingDepth > resource.maxNestingDepth };"#,
),
(
"forbid_output_blocked_fields",
"Block scripts that return output-blocked fields",
r#"forbid(principal, action, resource) when { principal.outputFields.containsAny(resource.outputBlockedFields) };"#,
),
(
"forbid_spread_without_declaration",
"Block scripts with spread in output when output declaration is required",
r#"forbid(principal, action, resource) when { principal.hasSpreadInOutput == true && resource.requireOutputDeclaration == true };"#,
),
(
"forbid_missing_output_declaration",
"Block scripts without output declaration when required",
r#"forbid(principal, action, resource) when { principal.hasOutputDeclaration == false && resource.requireOutputDeclaration == true };"#,
),
]
}
pub fn get_baseline_policies() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
(
"permit_reads",
"Permit all read operations (queries)",
r#"permit(principal, action == CodeMode::Action::"Read", resource);"#,
),
(
"permit_writes",
"Permit write operations (when enabled)",
r#"permit(principal, action == CodeMode::Action::"Write", resource) when { resource.allowWrite == true };"#,
),
(
"permit_deletes",
"Permit delete operations (when enabled)",
r#"permit(principal, action == CodeMode::Action::"Delete", resource) when { resource.allowDelete == true };"#,
),
(
"permit_admin",
"Permit admin operations (when enabled)",
r#"permit(principal, action == CodeMode::Action::"Admin", resource) when { resource.allowAdmin == true };"#,
),
(
"forbid_blocked_operations",
"Block operations in blocklist",
r#"forbid(principal, action, resource) when { resource.blockedOperations.contains(principal.operationName) };"#,
),
(
"forbid_blocked_fields",
"Block access to blocked fields",
r#"forbid(principal, action, resource) when { resource.blockedFields.containsAny(principal.accessedFields) };"#,
),
(
"forbid_excessive_depth",
"Enforce maximum query depth",
r#"forbid(principal, action, resource) when { principal.depth > resource.maxDepth };"#,
),
(
"forbid_excessive_cost",
"Enforce maximum query cost",
r#"forbid(principal, action, resource) when { principal.estimatedCost > resource.maxCost };"#,
),
]
}
#[cfg(all(test, feature = "openapi-code-mode"))]
mod tests {
use super::*;
use crate::config::{OperationEntry, OperationRegistry};
use crate::javascript::{ApiCall, HttpMethod, JavaScriptCodeInfo};
fn make_api_call(method: HttpMethod, path: &str) -> ApiCall {
ApiCall {
method,
path: path.to_string(),
is_dynamic_path: false,
line: 1,
column: 0,
}
}
fn make_info(calls: Vec<ApiCall>) -> JavaScriptCodeInfo {
JavaScriptCodeInfo {
api_calls: calls,
..Default::default()
}
}
fn make_registry(entries: &[(&str, &str, &str)]) -> OperationRegistry {
let entries: Vec<OperationEntry> = entries
.iter()
.map(|(id, category, path)| OperationEntry {
id: id.to_string(),
category: category.to_string(),
description: String::new(),
path: Some(path.to_string()),
})
.collect();
OperationRegistry::from_entries(&entries)
}
#[test]
fn test_category_read_overrides_post_method() {
let registry = make_registry(&[("getCostAnomalies", "read", "/getCostAnomalies")]);
let info = make_info(vec![make_api_call(HttpMethod::Post, "/getCostAnomalies")]);
let entity = ScriptEntity::from_javascript_info(&info, &[], Some(®istry));
assert_eq!(entity.read_calls, 1);
assert_eq!(entity.write_calls, 0);
assert_eq!(entity.script_type, "read_only");
assert_eq!(entity.action(), "Read");
}
#[test]
fn test_category_write_overrides_get_method() {
let registry = make_registry(&[("triggerExport", "write", "/triggerExport")]);
let info = make_info(vec![make_api_call(HttpMethod::Get, "/triggerExport")]);
let entity = ScriptEntity::from_javascript_info(&info, &[], Some(®istry));
assert_eq!(entity.write_calls, 1);
assert_eq!(entity.read_calls, 0);
assert_eq!(entity.script_type, "write_only");
assert_eq!(entity.action(), "Write");
}
#[test]
fn test_category_delete_routes_correctly() {
let registry = make_registry(&[("deleteReservation", "delete", "/deleteReservation")]);
let info = make_info(vec![make_api_call(HttpMethod::Post, "/deleteReservation")]);
let entity = ScriptEntity::from_javascript_info(&info, &[], Some(®istry));
assert_eq!(entity.delete_calls, 1);
assert_eq!(entity.has_deletes, true);
assert_eq!(entity.action(), "Delete");
}
#[test]
fn test_no_registry_falls_back_to_http_method() {
let info = make_info(vec![
make_api_call(HttpMethod::Get, "/getCostAnomalies"),
make_api_call(HttpMethod::Post, "/updateBudget"),
]);
let entity = ScriptEntity::from_javascript_info(&info, &[], None);
assert_eq!(entity.read_calls, 1);
assert_eq!(entity.write_calls, 1);
assert_eq!(entity.script_type, "mixed");
assert_eq!(entity.action(), "Write");
}
#[test]
fn test_unregistered_path_falls_back_to_http_method() {
let registry = make_registry(&[("getCostAnomalies", "read", "/getCostAnomalies")]);
let info = make_info(vec![make_api_call(HttpMethod::Post, "/unknownEndpoint")]);
let entity = ScriptEntity::from_javascript_info(&info, &[], Some(®istry));
assert_eq!(entity.write_calls, 1);
assert_eq!(entity.read_calls, 0);
assert_eq!(entity.script_type, "write_only");
}
#[test]
fn test_mixed_categories_produce_mixed_script() {
let registry = make_registry(&[
("getCostAnomalies", "read", "/getCostAnomalies"),
("updateBudget", "write", "/updateBudget"),
]);
let info = make_info(vec![
make_api_call(HttpMethod::Post, "/getCostAnomalies"),
make_api_call(HttpMethod::Post, "/updateBudget"),
]);
let entity = ScriptEntity::from_javascript_info(&info, &[], Some(®istry));
assert_eq!(entity.read_calls, 1);
assert_eq!(entity.write_calls, 1);
assert_eq!(entity.script_type, "mixed");
assert_eq!(entity.action(), "Write");
}
#[test]
fn test_empty_category_falls_back_to_http_method() {
let registry = make_registry(&[("legacyOp", "", "/legacyOp")]);
let info = make_info(vec![make_api_call(HttpMethod::Post, "/legacyOp")]);
let entity = ScriptEntity::from_javascript_info(&info, &[], Some(®istry));
assert_eq!(entity.write_calls, 1);
assert_eq!(entity.script_type, "write_only");
}
}