mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-11 10:43:31 +02:00
Compare commits
14 Commits
feat/capab
...
feat/allow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dfba14fbb | ||
|
|
f514d655b6 | ||
|
|
e729a9c83f | ||
|
|
41961e7de0 | ||
|
|
cb78c12db8 | ||
|
|
13a774e0ca | ||
|
|
d894ad2896 | ||
|
|
b382a4e08f | ||
|
|
b82899b96b | ||
|
|
708cb9fa28 | ||
|
|
8467f138bb | ||
|
|
50aaf4ec14 | ||
|
|
f8d98db9ff | ||
|
|
0811f512c5 |
@@ -17,13 +17,18 @@ pub use anyhow::Result;
|
||||
use cargo_toml::Manifest;
|
||||
use heck::AsShoutySnakeCase;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri_utils::{
|
||||
config::Config,
|
||||
namespace::{MemberResolution, NamespaceLockFile},
|
||||
plugin::{Capability, CapabilityOrList},
|
||||
resources::{external_binaries, resource_relpath, ResourcePaths},
|
||||
};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var_os,
|
||||
fs::write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -34,6 +39,8 @@ mod codegen;
|
||||
pub mod config;
|
||||
/// Mobile build functions.
|
||||
pub mod mobile;
|
||||
/// Build scripts for Tauri plugins.
|
||||
pub mod plugin;
|
||||
mod static_vcruntime;
|
||||
|
||||
#[cfg(feature = "codegen")]
|
||||
@@ -227,6 +234,7 @@ impl WindowsAttributes {
|
||||
pub struct Attributes {
|
||||
#[allow(dead_code)]
|
||||
windows_attributes: WindowsAttributes,
|
||||
capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
@@ -241,6 +249,36 @@ impl Attributes {
|
||||
self.windows_attributes = windows_attributes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends a capability JSON. See [`Capability`].
|
||||
#[must_use]
|
||||
pub fn capability_json(self, capability: impl AsRef<str>) -> Self {
|
||||
let capability: CapabilityOrList =
|
||||
serde_json::from_str(capability.as_ref()).expect("failed to deserialize capability");
|
||||
match capability {
|
||||
CapabilityOrList::Single(capability) => self.capability(capability),
|
||||
CapabilityOrList::List(l) => self.capabilities(l),
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a [`Capability`].
|
||||
#[must_use]
|
||||
pub fn capability(mut self, capability: Capability) -> Self {
|
||||
assert!(
|
||||
!capability.id.is_empty(),
|
||||
"capability must have an identifier"
|
||||
);
|
||||
self.capabilities.push(capability);
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends the given list of capabilities. See [`Self::capability`].
|
||||
pub fn capabilities<I: IntoIterator<Item = Capability>>(mut self, capabilities: I) -> Self {
|
||||
for capability in capabilities {
|
||||
self = self.capability(capability);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Run all build time helpers for your Tauri Application.
|
||||
@@ -293,9 +331,9 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
cfg_alias("desktop", !mobile);
|
||||
cfg_alias("mobile", mobile);
|
||||
|
||||
let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(
|
||||
std::env::current_dir().unwrap(),
|
||||
)?)?;
|
||||
let (config, config_path) =
|
||||
tauri_utils::config::parse::read_from(std::env::current_dir().unwrap())?;
|
||||
let mut config = serde_json::from_value(config)?;
|
||||
if let Ok(env) = std::env::var("TAURI_CONFIG") {
|
||||
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
|
||||
json_patch::merge(&mut config, &merge_config);
|
||||
@@ -473,10 +511,95 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut manifests = plugin::manifests();
|
||||
|
||||
const APP_MANIFEST_KEY: &str = "__app__";
|
||||
|
||||
manifests.insert(
|
||||
APP_MANIFEST_KEY.into(),
|
||||
tauri_utils::plugin::Manifest {
|
||||
plugin: "".into(),
|
||||
default_capability: None,
|
||||
capabilities: attributes.capabilities,
|
||||
features: Vec::new(),
|
||||
scope_type: Vec::new(),
|
||||
},
|
||||
);
|
||||
let mut resolution = HashMap::<String, MemberResolution>::new();
|
||||
|
||||
for namespace in &config.namespaces {
|
||||
for member in &namespace.members {
|
||||
let member_resolution =
|
||||
resolution
|
||||
.entry(member.clone())
|
||||
.or_insert_with(|| MemberResolution {
|
||||
member: member.clone(),
|
||||
commands: Default::default(),
|
||||
});
|
||||
|
||||
for capability in &namespace.capabilities {
|
||||
let (target_plugin, capability_id) = capability
|
||||
.split_once(':')
|
||||
.map(|(plugin, id)| (Some(plugin), id))
|
||||
.unwrap_or_else(|| (None, capability.as_str()));
|
||||
let capabilities = manifests.find_capability(capability_id);
|
||||
|
||||
if capabilities.is_empty() {
|
||||
panic!("could not find capability specification matching id {capability}")
|
||||
} else {
|
||||
let (plugin, capability) = if let Some(target) = target_plugin {
|
||||
capabilities
|
||||
.into_iter()
|
||||
.find(|(p, _)| p == target)
|
||||
.unwrap_or_else(|| {
|
||||
panic!("failed to find capability matching id {capability_id} for plugin {target}")
|
||||
})
|
||||
} else if capabilities.len() > 1 {
|
||||
panic!(
|
||||
"found a conflict on capability id {capability}, please use one of the [{}] prefixes",
|
||||
capabilities
|
||||
.iter()
|
||||
.map(|(p, _)| format!("'{p}:'"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
} else {
|
||||
// already checked that the capabilities aren't empty
|
||||
capabilities.into_iter().next().unwrap()
|
||||
};
|
||||
|
||||
if plugin == APP_MANIFEST_KEY {
|
||||
member_resolution.commands.extend(capability.features);
|
||||
} else {
|
||||
member_resolution.commands.extend(
|
||||
capability
|
||||
.features
|
||||
.into_iter()
|
||||
.map(|f| format!("plugin:{plugin}|{f}"))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lockfile = NamespaceLockFile {
|
||||
version: 1,
|
||||
namespaces: config.namespaces,
|
||||
plugins: manifests,
|
||||
resolution: resolution.into_values().collect(),
|
||||
};
|
||||
write(
|
||||
config_path.parent().unwrap().join("tauri.namespace.lock"),
|
||||
serde_json::to_string_pretty(&lockfile)?,
|
||||
)
|
||||
.context("failed to write namespace lockfile")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
struct CargoMetadata {
|
||||
workspace_root: PathBuf,
|
||||
}
|
||||
|
||||
70
core/tauri-build/src/plugin.rs
Normal file
70
core/tauri-build/src/plugin.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use tauri_utils::plugin::Capability;
|
||||
pub use tauri_utils::plugin::{Manifest, ManifestMap, ScopeType};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env::{var_os, vars_os},
|
||||
fs::{read_to_string, write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
const PLUGIN_METADATA_KEY: &str = "PLUGIN_MANIFEST_PATH";
|
||||
|
||||
pub fn set_manifest(mut manifest: Manifest) {
|
||||
for feature in &manifest.features {
|
||||
let feature_capability_id = format!("allow-{feature}");
|
||||
if !manifest
|
||||
.capabilities
|
||||
.iter()
|
||||
.any(|c| c.id == feature_capability_id)
|
||||
{
|
||||
manifest.capabilities.push(Capability {
|
||||
id: feature_capability_id,
|
||||
component: None,
|
||||
description: Some(format!("Allows the {feature} functionality")),
|
||||
features: vec![feature.clone()],
|
||||
scope: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let manifest_str = serde_json::to_string(&manifest).expect("failed to serialize plugin manifest");
|
||||
let manifest_path = var_os("OUT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.expect(
|
||||
"missing OUT_DIR environment variable.. are you sure you are running this on a build script?",
|
||||
)
|
||||
.join(format!("{}-plugin-manifest.json", manifest.plugin));
|
||||
write(&manifest_path, manifest_str).expect("failed to save manifest file");
|
||||
|
||||
println!(
|
||||
"cargo:{}_{PLUGIN_METADATA_KEY}={}",
|
||||
manifest.plugin,
|
||||
manifest_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn manifests() -> ManifestMap {
|
||||
let mut manifests = BTreeMap::new();
|
||||
|
||||
for (key, value) in vars_os() {
|
||||
let key = key.to_string_lossy();
|
||||
if let Some(_plugin_crate_name) = key
|
||||
.strip_prefix("DEP_")
|
||||
.and_then(|v| v.strip_suffix(&format!("_{PLUGIN_METADATA_KEY}")))
|
||||
{
|
||||
let plugin_manifest_path = PathBuf::from(value);
|
||||
let plugin_manifest_str =
|
||||
read_to_string(&plugin_manifest_path).expect("failed to read plugin manifest");
|
||||
let manifest: Manifest =
|
||||
serde_json::from_str(&plugin_manifest_str).expect("failed to deserialize plugin manifest");
|
||||
manifests.insert(manifest.plugin.clone(), manifest);
|
||||
}
|
||||
}
|
||||
|
||||
manifests.into()
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{ffi::OsStr, str::FromStr};
|
||||
use std::{ffi::OsStr, fs::read_to_string, str::FromStr};
|
||||
|
||||
use base64::Engine;
|
||||
use proc_macro2::TokenStream;
|
||||
@@ -15,6 +15,7 @@ use tauri_utils::config::{AppUrl, Config, PatternKind, WindowUrl};
|
||||
use tauri_utils::html::{
|
||||
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
|
||||
};
|
||||
use tauri_utils::namespace::NamespaceLockFile;
|
||||
|
||||
use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};
|
||||
|
||||
@@ -421,6 +422,15 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
}
|
||||
};
|
||||
|
||||
let lockfile_path = config_parent.join("tauri.namespace.lock");
|
||||
let lockfile: NamespaceLockFile = if lockfile_path.exists() {
|
||||
let lockfile = read_to_string(&lockfile_path)?;
|
||||
serde_json::from_str(&lockfile)?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let runtime_authority = runtime_authority_codegen(&root, lockfile);
|
||||
|
||||
Ok(quote!({
|
||||
#[allow(unused_mut, clippy::let_and_return)]
|
||||
let mut context = #root::Context::new(
|
||||
@@ -431,12 +441,30 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
#package_info,
|
||||
#info_plist,
|
||||
#pattern,
|
||||
#runtime_authority
|
||||
);
|
||||
#with_system_tray_icon_code
|
||||
context
|
||||
}))
|
||||
}
|
||||
|
||||
fn runtime_authority_codegen(root: &TokenStream, lockfile: NamespaceLockFile) -> TokenStream {
|
||||
let add_members = lockfile.resolution.iter().map(|r| {
|
||||
let member = &r.member;
|
||||
let commands = &r.commands;
|
||||
let resolution = quote!(#root::runtime_authority::MemberResolution {
|
||||
member: #member.into(),
|
||||
commands: vec![#(#commands.into(),)*]
|
||||
});
|
||||
quote!(authority.add_member(#resolution);)
|
||||
});
|
||||
quote!({
|
||||
let mut authority = #root::runtime_authority::RuntimeAuthority::new();
|
||||
#(#add_members)*
|
||||
authority
|
||||
})
|
||||
}
|
||||
|
||||
fn ico_icon<P: AsRef<Path>>(
|
||||
root: &TokenStream,
|
||||
out_dir: &Path,
|
||||
|
||||
@@ -59,6 +59,12 @@ pub enum EmbeddedAssetsError {
|
||||
|
||||
#[error("version error: {0}")]
|
||||
Version(#[from] semver::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Represent a directory of assets that are compressed and embedded.
|
||||
|
||||
@@ -45,7 +45,7 @@ pub enum CodegenConfigError {
|
||||
ConfigError(#[from] ConfigError),
|
||||
}
|
||||
|
||||
/// Get the [`Config`] from the `TAURI_CONFIG` environmental variable, or read from the passed path.
|
||||
/// Get the [`Config`] from the passed path and merge it with the value from the `TAURI_CONFIG` environment variable.
|
||||
///
|
||||
/// If the passed path is relative, it should be relative to the current working directory of the
|
||||
/// compiling crate.
|
||||
@@ -67,7 +67,8 @@ pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError>
|
||||
// it is impossible for the content of two separate configs to get mixed up. The chances are
|
||||
// already unlikely unless the developer goes out of their way to run the cli on a different
|
||||
// project than the target crate.
|
||||
let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(parent.clone())?)?;
|
||||
let (config, config_path) = tauri_utils::config::parse::read_from(parent.clone())?;
|
||||
let mut config = serde_json::from_value(config)?;
|
||||
if let Ok(env) = std::env::var("TAURI_CONFIG") {
|
||||
let merge_config: serde_json::Value =
|
||||
serde_json::from_str(&env).map_err(CodegenConfigError::FormatInline)?;
|
||||
@@ -83,5 +84,11 @@ pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError>
|
||||
// Reset working directory.
|
||||
std::env::set_current_dir(old_cwd).map_err(CodegenConfigError::CurrentDir)?;
|
||||
|
||||
Ok((config, parent))
|
||||
Ok((
|
||||
config,
|
||||
config_path
|
||||
.parent()
|
||||
.map(ToOwned::to_owned)
|
||||
.ok_or_else(|| CodegenConfigError::Parent(config_path))?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -108,6 +108,14 @@
|
||||
"$ref": "#/definitions/PluginConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"namespaces": {
|
||||
"description": "The namespaces defining what capabilities are enabled.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Namespace"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -2258,6 +2266,43 @@
|
||||
"description": "The plugin configs holds a HashMap mapping a plugin name to its configuration object.\n\nSee more: https://tauri.app/v1/api/config#pluginconfig",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"Namespace": {
|
||||
"description": "A namespace defining a set of capabilities that are enabled for a given window.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"capabilities",
|
||||
"id",
|
||||
"members"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Identifier of this namespace. Must be unique.\n\nIt is recommended to use `drop-` or `allow-` prefixes to ensure the rule can be easily categorized.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Describes the namespace in a human readable format.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"members": {
|
||||
"description": "The windows that can use the configuration of this namespace.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"description": "List of capabilities attached to this namespace.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1916,6 +1916,27 @@ pub struct Config {
|
||||
/// The plugins config.
|
||||
#[serde(default)]
|
||||
pub plugins: PluginConfig,
|
||||
/// The namespaces defining what capabilities are enabled.
|
||||
#[serde(default)]
|
||||
pub namespaces: Vec<Namespace>,
|
||||
}
|
||||
|
||||
/// A namespace defining a set of capabilities that are enabled for a given window.
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Namespace {
|
||||
/// Identifier of this namespace. Must be unique.
|
||||
///
|
||||
/// It is recommended to use `drop-` or `allow-` prefixes to ensure the rule can be easily categorized.
|
||||
pub id: String,
|
||||
/// Describes the namespace in a human readable format.
|
||||
pub description: Option<String>,
|
||||
/// The windows that can use the configuration of this namespace.
|
||||
pub members: Vec<String>,
|
||||
/// List of capabilities attached to this namespace.
|
||||
pub capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
/// The plugin configs holds a HashMap mapping a plugin name to its configuration object.
|
||||
@@ -2646,6 +2667,17 @@ mod build {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Namespace {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let id = str_lit(&self.id);
|
||||
let description = opt_str_lit(self.description.as_ref());
|
||||
let members = vec_lit(&self.members, str_lit);
|
||||
let capabilities = vec_lit(&self.capabilities, str_lit);
|
||||
|
||||
literal_struct!(tokens, Namespace, id, description, members, capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Config {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let schema = quote!(None);
|
||||
@@ -2653,8 +2685,9 @@ mod build {
|
||||
let tauri = &self.tauri;
|
||||
let build = &self.build;
|
||||
let plugins = &self.plugins;
|
||||
let namespaces = vec_lit(&self.namespaces, identity);
|
||||
|
||||
literal_struct!(tokens, Config, schema, package, tauri, build, plugins);
|
||||
literal_struct!(tokens, Config, schema, package, tauri, build, plugins, namespaces);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,12 +192,12 @@ pub fn is_configuration_file(path: &Path) -> bool {
|
||||
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
|
||||
///
|
||||
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
|
||||
pub fn read_from(root_dir: PathBuf) -> Result<Value, ConfigError> {
|
||||
let mut config: Value = parse_value(root_dir.join("tauri.conf.json"))?.0;
|
||||
pub fn read_from(root_dir: PathBuf) -> Result<(Value, PathBuf), ConfigError> {
|
||||
let (mut config, path) = parse_value(root_dir.join("tauri.conf.json"))?;
|
||||
if let Some((platform_config, _)) = read_platform(root_dir)? {
|
||||
merge(&mut config, &platform_config);
|
||||
}
|
||||
Ok(config)
|
||||
Ok((config, path))
|
||||
}
|
||||
|
||||
/// Reads the platform-specific configuration file from the given root directory if it exists.
|
||||
|
||||
@@ -26,7 +26,9 @@ pub mod config;
|
||||
pub mod html;
|
||||
pub mod io;
|
||||
pub mod mime_type;
|
||||
pub mod namespace;
|
||||
pub mod platform;
|
||||
pub mod plugin;
|
||||
/// Prepare application resources and sidecars.
|
||||
#[cfg(feature = "resources")]
|
||||
pub mod resources;
|
||||
|
||||
31
core/tauri-utils/src/namespace.rs
Normal file
31
core/tauri-utils/src/namespace.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Namespace lock file and utilities for the runtime authority.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{config::Namespace, plugin::ManifestMap};
|
||||
|
||||
/// Resolved data associated with a member.
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct MemberResolution {
|
||||
/// Member id.
|
||||
pub member: String,
|
||||
/// List of commands enabled.
|
||||
pub commands: Vec<String>,
|
||||
}
|
||||
|
||||
/// Lock file of the namespaces configuration.
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct NamespaceLockFile {
|
||||
/// Lock file version.
|
||||
pub version: u8,
|
||||
/// Configured namespaces.
|
||||
pub namespaces: Vec<Namespace>,
|
||||
/// Collection of plugins and their manifests.
|
||||
pub plugins: ManifestMap,
|
||||
/// Resolved data.
|
||||
pub resolution: Vec<MemberResolution>,
|
||||
}
|
||||
201
core/tauri-utils/src/plugin.rs
Normal file
201
core/tauri-utils/src/plugin.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Plugin manifest types.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
const DEFAULT_CAPABILITY_ID: &str = "default";
|
||||
|
||||
/// Scope type definition.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum ScopeType {
|
||||
/// String type.
|
||||
String,
|
||||
}
|
||||
|
||||
/// Scope of a given capability.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CapabilityScope {
|
||||
/// Explicitly allow something.
|
||||
#[serde(default)]
|
||||
pub allowed: Vec<serde_json::Value>,
|
||||
/// Explicitly deny something. Takes precedence over [`Self::allowed`].
|
||||
#[serde(default)]
|
||||
pub blocked: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// A capability defines a set of features and scope enabled for the plugin.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Capability {
|
||||
/// The identifier of the capability. Must be unique.
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
/// The component this capability refers to.
|
||||
///
|
||||
/// Currently the possible values are plugin names.
|
||||
///
|
||||
/// When no value is set, it referes to the application itself.
|
||||
pub component: Option<String>,
|
||||
/// Describes the capability in a human readable format.
|
||||
pub description: Option<String>,
|
||||
/// List of features enabled by this capability.
|
||||
#[serde(default)]
|
||||
pub features: Vec<String>,
|
||||
/// Scope defined by this capability. Only applies to the given features.
|
||||
#[serde(default)]
|
||||
pub scope: CapabilityScope,
|
||||
}
|
||||
|
||||
/// An enum used to do serde operations with a list or a single capability.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CapabilityOrList {
|
||||
/// A single capability.
|
||||
Single(Capability),
|
||||
/// A list of capabilities.
|
||||
List(Vec<Capability>),
|
||||
}
|
||||
|
||||
/// Plugin manifest.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
/// Plugin name.
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub plugin: String,
|
||||
/// Default capability.
|
||||
pub default_capability: Option<Capability>,
|
||||
/// List of capabilities defined by the plugin.
|
||||
pub capabilities: Vec<Capability>,
|
||||
/// List of features defined by the plugin.
|
||||
pub features: Vec<String>,
|
||||
/// Scope types.
|
||||
pub scope_type: Vec<ScopeType>,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
/// Creates a new empty plugin manifest.
|
||||
pub fn new(plugin: impl Into<String>) -> Self {
|
||||
Self {
|
||||
plugin: plugin.into(),
|
||||
default_capability: None,
|
||||
capabilities: Vec::new(),
|
||||
features: Vec::new(),
|
||||
scope_type: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the plugin's default capability set from a JSON string.
|
||||
pub fn default_capability_json(mut self, default_capability: impl AsRef<str>) -> Self {
|
||||
let mut capability: Capability = serde_json::from_str(default_capability.as_ref())
|
||||
.expect("failed to deserialize default capability");
|
||||
assert!(
|
||||
capability.id.is_empty(),
|
||||
"default capability cannot have an identifier"
|
||||
);
|
||||
capability.id = DEFAULT_CAPABILITY_ID.into();
|
||||
self.default_capability.replace(capability);
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends a capability from a JSON string. The JSON can also include an array of capabilities instead of a single one. See [`Capability`].
|
||||
pub fn capability_json(self, capability: impl AsRef<str>) -> Self {
|
||||
let capability =
|
||||
serde_json::from_str(capability.as_ref()).expect("failed to deserialize default capability");
|
||||
match capability {
|
||||
CapabilityOrList::Single(cap) => self.capability(cap),
|
||||
CapabilityOrList::List(l) => self.capabilities(l),
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a [`Capability`].
|
||||
pub fn capability(mut self, capability: Capability) -> Self {
|
||||
assert!(
|
||||
!capability.id.is_empty(),
|
||||
"capability must have an identifier"
|
||||
);
|
||||
self.capabilities.push(capability);
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends the given list of capabilities. See [`Self::capability`].
|
||||
pub fn capabilities<I: IntoIterator<Item = Capability>>(mut self, capabilities: I) -> Self {
|
||||
for capability in capabilities {
|
||||
self = self.capability(capability);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends the given feature on the list of plugin's features.
|
||||
pub fn feature(mut self, feature: impl Into<String>) -> Self {
|
||||
self.features.push(feature.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends the given list of features.
|
||||
pub fn features<I: IntoIterator<Item = S>, S: Into<String>>(mut self, features: I) -> Self {
|
||||
for feature in features {
|
||||
self = self.feature(feature);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends the given scope type.
|
||||
pub fn scope_type(mut self, ty: ScopeType) -> Self {
|
||||
self.scope_type.push(ty);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection mapping a plugin name to its manifest.
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct ManifestMap(BTreeMap<String, Manifest>);
|
||||
|
||||
impl Deref for ManifestMap {
|
||||
type Target = BTreeMap<String, Manifest>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ManifestMap {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<String, Manifest>> for ManifestMap {
|
||||
fn from(value: BTreeMap<String, Manifest>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManifestMap {
|
||||
/// Finds the capability with the given identifier.
|
||||
pub fn find_capability(&self, id: &str) -> Vec<(String, Capability)> {
|
||||
let mut capabilities = Vec::new();
|
||||
|
||||
for (plugin, manifest) in &self.0 {
|
||||
if id == format!("{DEFAULT_CAPABILITY_ID}-{plugin}") {
|
||||
capabilities.push((
|
||||
plugin.clone(),
|
||||
manifest.default_capability.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
for capability in &manifest.capabilities {
|
||||
if capability.id == id {
|
||||
capabilities.push((plugin.clone(), capability.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
capabilities
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,12 @@ fn main() {
|
||||
println!("cargo:ios_library_path={}", lib_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
tauri_build::plugin::set_manifest(
|
||||
tauri_build::plugin::Manifest::new("event").features(["emit", "listen"]),
|
||||
);
|
||||
|
||||
tauri_build::plugin::set_manifest(tauri_build::plugin::Manifest::new("path"));
|
||||
}
|
||||
|
||||
fn add_manifest() {
|
||||
|
||||
@@ -66,6 +66,7 @@ pub use cocoa;
|
||||
pub use embed_plist;
|
||||
/// The Tauri error enum.
|
||||
pub use error::Error;
|
||||
use runtime_authority::RuntimeAuthority;
|
||||
#[cfg(target_os = "ios")]
|
||||
#[doc(hidden)]
|
||||
pub use swift_rs;
|
||||
@@ -85,6 +86,7 @@ mod hooks;
|
||||
mod manager;
|
||||
mod pattern;
|
||||
pub mod plugin;
|
||||
pub mod runtime_authority;
|
||||
mod vibrancy;
|
||||
pub mod window;
|
||||
use tauri_runtime as runtime;
|
||||
@@ -389,6 +391,7 @@ pub struct Context<A: Assets> {
|
||||
pub(crate) package_info: PackageInfo,
|
||||
pub(crate) _info_plist: (),
|
||||
pub(crate) pattern: Pattern,
|
||||
pub(crate) runtime_authority: RuntimeAuthority,
|
||||
}
|
||||
|
||||
impl<A: Assets> fmt::Debug for Context<A> {
|
||||
@@ -487,6 +490,7 @@ impl<A: Assets> Context<A> {
|
||||
package_info: PackageInfo,
|
||||
info_plist: (),
|
||||
pattern: Pattern,
|
||||
runtime_authority: RuntimeAuthority,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
@@ -498,6 +502,7 @@ impl<A: Assets> Context<A> {
|
||||
package_info,
|
||||
_info_plist: info_plist,
|
||||
pattern,
|
||||
runtime_authority,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ use tauri_utils::{
|
||||
html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
};
|
||||
|
||||
use crate::app::{GlobalMenuEventListener, WindowMenuEvent};
|
||||
use crate::hooks::IpcJavascript;
|
||||
#[cfg(feature = "isolation")]
|
||||
use crate::hooks::IsolationJavascript;
|
||||
@@ -51,6 +50,10 @@ use crate::{
|
||||
Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window,
|
||||
WindowEvent,
|
||||
};
|
||||
use crate::{
|
||||
app::{GlobalMenuEventListener, WindowMenuEvent},
|
||||
runtime_authority::RuntimeAuthority,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use crate::path::BaseDirectory;
|
||||
@@ -234,6 +237,7 @@ pub struct InnerWindowManager<R: Runtime> {
|
||||
invoke_initialization_script: String,
|
||||
/// Application pattern.
|
||||
pub(crate) pattern: Pattern,
|
||||
pub(crate) runtime_authority: RuntimeAuthority,
|
||||
}
|
||||
|
||||
impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
|
||||
@@ -334,6 +338,7 @@ impl<R: Runtime> WindowManager<R> {
|
||||
window_event_listeners: Arc::new(window_event_listeners),
|
||||
invoke_responder,
|
||||
invoke_initialization_script,
|
||||
runtime_authority: context.runtime_authority,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
30
core/tauri/src/runtime_authority.rs
Normal file
30
core/tauri/src/runtime_authority.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
//! Runtime authority.
|
||||
|
||||
pub use tauri_utils::namespace::MemberResolution;
|
||||
|
||||
/// The runtime authority verifies if a given IPC call is authorized.
|
||||
#[derive(Default)]
|
||||
pub struct RuntimeAuthority {
|
||||
members: Vec<MemberResolution>,
|
||||
}
|
||||
|
||||
impl RuntimeAuthority {
|
||||
/// Creates the default (empty) runtime authority.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Adds the given member resolution to this authority.
|
||||
pub fn add_member(&mut self, member: MemberResolution) {
|
||||
self.members.push(member);
|
||||
}
|
||||
|
||||
/// Determines if the given command is allowed for the member.
|
||||
pub fn is_allowed(&self, member: &str, command: &String) -> bool {
|
||||
if let Some(member) = self.members.iter().find(|m| m.member == member) {
|
||||
member.commands.contains(command)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,17 +166,27 @@ impl Scope {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tauri_utils::namespace::MemberResolution;
|
||||
|
||||
use super::RemoteDomainAccessScope;
|
||||
use crate::{
|
||||
api::ipc::CallbackFn,
|
||||
test::{assert_ipc_response, mock_app, MockRuntime},
|
||||
test::{assert_ipc_response, mock_builder, mock_context, noop_assets, MockRuntime},
|
||||
App, InvokePayload, Manager, Window, WindowBuilder,
|
||||
};
|
||||
|
||||
const PLUGIN_NAME: &str = "test";
|
||||
|
||||
fn test_context(scopes: Vec<RemoteDomainAccessScope>) -> (App<MockRuntime>, Window<MockRuntime>) {
|
||||
let app = mock_app();
|
||||
let mut context = mock_context(noop_assets());
|
||||
context.runtime_authority.add_member(MemberResolution {
|
||||
member: "main".into(),
|
||||
commands: vec![
|
||||
"plugin:path|is_absolute".into(),
|
||||
format!("plugin:{PLUGIN_NAME}|doSomething"),
|
||||
],
|
||||
});
|
||||
let app = mock_builder().build(context).unwrap();
|
||||
let window = WindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -132,6 +132,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
|
||||
},
|
||||
build: Default::default(),
|
||||
plugins: Default::default(),
|
||||
namespaces: Default::default(),
|
||||
},
|
||||
assets: Arc::new(assets),
|
||||
default_window_icon: None,
|
||||
@@ -147,6 +148,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
|
||||
},
|
||||
_info_plist: (),
|
||||
pattern: Pattern::Brownfield(std::marker::PhantomData),
|
||||
runtime_authority: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1691,6 +1691,15 @@ impl<R: Runtime> Window<R> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !manager
|
||||
.inner
|
||||
.runtime_authority
|
||||
.is_allowed(self.label(), &payload.cmd)
|
||||
{
|
||||
invoke.resolver.reject("Not allowed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if payload.cmd.starts_with("plugin:") {
|
||||
if !is_local {
|
||||
let command = invoke.message.command.replace("plugin:", "");
|
||||
|
||||
677
examples/api/src-tauri/Cargo.lock
generated
677
examples/api/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,15 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
fn main() {
|
||||
tauri_build::try_build(
|
||||
tauri_build::Attributes::new()
|
||||
.capability_json(include_str!("./capabilities/allow-commands.json")),
|
||||
)
|
||||
.expect("failed to run tauri-build");
|
||||
|
||||
let mut codegen = tauri_build::CodegenContext::new();
|
||||
if !cfg!(feature = "custom-protocol") {
|
||||
codegen = codegen.dev();
|
||||
}
|
||||
codegen.build();
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
8
examples/api/src-tauri/capabilities/allow-commands.json
Normal file
8
examples/api/src-tauri/capabilities/allow-commands.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "allow-all-api-commands",
|
||||
"description": "Allows all application defined commands",
|
||||
"features": [
|
||||
"log_operation",
|
||||
"perform_request"
|
||||
]
|
||||
}
|
||||
1452
examples/api/src-tauri/tauri-plugin-sample/Cargo.lock
generated
1452
examples/api/src-tauri/tauri-plugin-sample/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::process::exit;
|
||||
use tauri_build::{
|
||||
mobile::PluginBuilder,
|
||||
plugin::{set_manifest, Manifest, ScopeType},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = tauri_build::mobile::PluginBuilder::new()
|
||||
if let Err(error) = PluginBuilder::new()
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.run()
|
||||
@@ -13,4 +17,12 @@ fn main() {
|
||||
println!("{error:#}");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
set_manifest(
|
||||
Manifest::new("sample")
|
||||
.default_capability_json(include_str!("capabilities/default.json"))
|
||||
.capability_json(include_str!("capabilities/ping.json"))
|
||||
.feature("ping")
|
||||
.scope_type(ScopeType::String),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"features": [],
|
||||
"description": "Default empty capability set",
|
||||
"scope": {
|
||||
"allowed": [],
|
||||
"denied": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "allow-ping",
|
||||
"description": "Allows the ping command",
|
||||
"features": [
|
||||
"ping"
|
||||
]
|
||||
}
|
||||
@@ -106,5 +106,11 @@
|
||||
"iconAsTemplate": true,
|
||||
"menuOnLeftClick": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespaces": [{
|
||||
"id": "main",
|
||||
"description": "Main window namespace",
|
||||
"members": ["main"],
|
||||
"capabilities": ["allow-all-api-commands", "allow-ping", "allow-emit", "allow-listen"]
|
||||
}]
|
||||
}
|
||||
|
||||
127
examples/api/src-tauri/tauri.namespace.lock
Normal file
127
examples/api/src-tauri/tauri.namespace.lock
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"version": 1,
|
||||
"namespaces": [
|
||||
{
|
||||
"id": "main",
|
||||
"description": "Main window namespace",
|
||||
"members": [
|
||||
"main"
|
||||
],
|
||||
"capabilities": [
|
||||
"allow-all-api-commands",
|
||||
"allow-ping",
|
||||
"allow-emit",
|
||||
"allow-listen"
|
||||
]
|
||||
}
|
||||
],
|
||||
"plugins": {
|
||||
"__app__": {
|
||||
"default_capability": null,
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "allow-all-api-commands",
|
||||
"component": null,
|
||||
"description": "Allows all application defined commands",
|
||||
"features": [
|
||||
"log_operation",
|
||||
"perform_request"
|
||||
],
|
||||
"scope": {
|
||||
"allowed": [],
|
||||
"blocked": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": [],
|
||||
"scope_type": []
|
||||
},
|
||||
"event": {
|
||||
"plugin": "event",
|
||||
"default_capability": null,
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "allow-emit",
|
||||
"component": null,
|
||||
"description": "Allows the emit functionality",
|
||||
"features": [
|
||||
"emit"
|
||||
],
|
||||
"scope": {
|
||||
"allowed": [],
|
||||
"blocked": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "allow-listen",
|
||||
"component": null,
|
||||
"description": "Allows the listen functionality",
|
||||
"features": [
|
||||
"listen"
|
||||
],
|
||||
"scope": {
|
||||
"allowed": [],
|
||||
"blocked": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"emit",
|
||||
"listen"
|
||||
],
|
||||
"scope_type": []
|
||||
},
|
||||
"path": {
|
||||
"plugin": "path",
|
||||
"default_capability": null,
|
||||
"capabilities": [],
|
||||
"features": [],
|
||||
"scope_type": []
|
||||
},
|
||||
"sample": {
|
||||
"plugin": "sample",
|
||||
"default_capability": {
|
||||
"id": "default",
|
||||
"component": null,
|
||||
"description": "Default empty capability set",
|
||||
"features": [],
|
||||
"scope": {
|
||||
"allowed": [],
|
||||
"blocked": []
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "allow-ping",
|
||||
"component": null,
|
||||
"description": "Allows the ping command",
|
||||
"features": [
|
||||
"ping"
|
||||
],
|
||||
"scope": {
|
||||
"allowed": [],
|
||||
"blocked": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"ping"
|
||||
],
|
||||
"scope_type": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
},
|
||||
"resolution": [
|
||||
{
|
||||
"member": "main",
|
||||
"commands": [
|
||||
"log_operation",
|
||||
"perform_request",
|
||||
"plugin:sample|ping",
|
||||
"plugin:event|emit",
|
||||
"plugin:event|listen"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
4
tooling/cli/Cargo.lock
generated
4
tooling/cli/Cargo.lock
generated
@@ -3907,9 +3907,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.38"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
|
||||
checksum = "ec96d2ffad078296368d46ff1cb309be1c23c513b4ab0e22a45de0185275ac96"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
|
||||
@@ -108,6 +108,14 @@
|
||||
"$ref": "#/definitions/PluginConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"namespaces": {
|
||||
"description": "The namespaces defining what capabilities are enabled.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Namespace"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -2258,6 +2266,43 @@
|
||||
"description": "The plugin configs holds a HashMap mapping a plugin name to its configuration object.\n\nSee more: https://tauri.app/v1/api/config#pluginconfig",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"Namespace": {
|
||||
"description": "A namespace defining a set of capabilities that are enabled for a given window.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"capabilities",
|
||||
"id",
|
||||
"members"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Identifier of this namespace. Must be unique.\n\nIt is recommended to use `drop-` or `allow-` prefixes to ensure the rule can be easily categorized.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Describes the namespace in a human readable format.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"members": {
|
||||
"description": "The windows that can use the configuration of this namespace.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"description": "List of capabilities attached to this namespace.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user