Skip to main content

gsm_core/
pack_extensions.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::io::Read;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use greentic_types::pack_manifest::{ExtensionInline, ExtensionRef, PackManifest};
8use serde::{Deserialize, Serialize};
9
10use crate::path_safety::normalize_under_root;
11
12pub const INGRESS_EXTENSION_ID: &str = "messaging.provider_ingress.v1";
13pub const OAUTH_EXTENSION_ID: &str = "messaging.oauth.v1";
14pub const SUBSCRIPTIONS_EXTENSION_ID: &str = "messaging.subscriptions.v1";
15
16#[derive(Clone, Debug, Deserialize, Serialize)]
17pub struct RuntimeRef {
18    pub component_ref: String,
19    pub export: String,
20    pub world: String,
21}
22
23#[derive(Clone, Debug, Default, Deserialize, Serialize)]
24pub struct IngressCapabilities {
25    #[serde(default)]
26    pub supports_webhook_validation: bool,
27    #[serde(default)]
28    pub content_types: Vec<String>,
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize)]
32pub struct IngressProviderDecl {
33    pub runtime: RuntimeRef,
34    #[serde(default)]
35    pub capabilities: IngressCapabilities,
36}
37
38#[derive(Clone, Debug, Deserialize, Serialize)]
39pub struct OAuthProviderDecl {
40    pub provider: String,
41    #[serde(default)]
42    pub scopes: Vec<String>,
43    #[serde(default)]
44    pub resource: Option<String>,
45    #[serde(default)]
46    pub prompt: Option<String>,
47    #[serde(default)]
48    pub redirect_path: Option<String>,
49}
50
51#[derive(Clone, Debug, Deserialize, Serialize)]
52pub struct SubscriptionsProviderDecl {
53    pub runtime: RuntimeRef,
54    #[serde(default)]
55    pub resources: Vec<String>,
56    #[serde(default)]
57    pub renewal_window_hours: Option<u32>,
58}
59
60#[derive(Clone, Debug, Default)]
61pub struct ProviderExtensionsRegistry {
62    pub ingress: BTreeMap<String, IngressProviderDecl>,
63    pub oauth: BTreeMap<String, OAuthProviderDecl>,
64    pub subscriptions: BTreeMap<String, SubscriptionsProviderDecl>,
65}
66
67impl ProviderExtensionsRegistry {
68    pub fn is_empty(&self) -> bool {
69        self.ingress.is_empty() && self.oauth.is_empty() && self.subscriptions.is_empty()
70    }
71}
72
73#[derive(Debug, Deserialize)]
74struct PackSpecExtensions {
75    #[allow(dead_code)]
76    id: String,
77    #[allow(dead_code)]
78    version: String,
79    #[serde(default)]
80    extensions: Option<BTreeMap<String, ExtensionRef>>,
81}
82
83#[derive(Debug, Deserialize)]
84struct IngressPayload {
85    #[serde(flatten)]
86    providers: BTreeMap<String, IngressProviderDecl>,
87}
88
89#[derive(Debug, Deserialize)]
90struct OAuthPayload {
91    #[serde(flatten)]
92    providers: BTreeMap<String, OAuthProviderDecl>,
93}
94
95#[derive(Debug, Deserialize)]
96struct SubscriptionsPayload {
97    #[serde(flatten)]
98    providers: BTreeMap<String, SubscriptionsProviderDecl>,
99}
100
101pub fn load_provider_extensions_from_pack_files(
102    root: &Path,
103    paths: &[PathBuf],
104) -> Result<ProviderExtensionsRegistry> {
105    let root = root
106        .canonicalize()
107        .with_context(|| format!("failed to canonicalize packs root {}", root.display()))?;
108    let mut registry = ProviderExtensionsRegistry::default();
109    for path in paths {
110        let extensions = extensions_from_pack_file(&root, path)
111            .with_context(|| format!("failed to read pack extensions from {}", path.display()))?;
112        merge_registry(&mut registry, extensions);
113    }
114    Ok(registry)
115}
116
117fn merge_registry(target: &mut ProviderExtensionsRegistry, incoming: ProviderExtensionsRegistry) {
118    target.ingress.extend(incoming.ingress);
119    target.oauth.extend(incoming.oauth);
120    target.subscriptions.extend(incoming.subscriptions);
121}
122
123fn extensions_from_pack_file(root: &Path, path: &Path) -> Result<ProviderExtensionsRegistry> {
124    let safe_path = if path.is_absolute() {
125        path.canonicalize()
126            .with_context(|| format!("failed to canonicalize {}", path.display()))?
127    } else {
128        normalize_under_root(root, path)?
129    };
130    let ext = safe_path
131        .extension()
132        .and_then(|s| s.to_str())
133        .map(|s| s.to_ascii_lowercase());
134    match ext.as_deref() {
135        Some("gtpack") => extensions_from_gtpack(&safe_path),
136        _ => extensions_from_pack_yaml(&safe_path),
137    }
138}
139
140fn extensions_from_pack_yaml(path: &Path) -> Result<ProviderExtensionsRegistry> {
141    let raw = fs::read_to_string(path)
142        .with_context(|| format!("failed to read pack file {}", path.display()))?;
143    let spec: PackSpecExtensions = serde_yaml_bw::from_str(&raw)
144        .with_context(|| format!("{} is not a valid pack spec", path.display()))?;
145    let mut registry = ProviderExtensionsRegistry::default();
146    apply_extensions(&mut registry, spec.extensions.as_ref());
147    Ok(registry)
148}
149
150fn extensions_from_gtpack(path: &Path) -> Result<ProviderExtensionsRegistry> {
151    let manifest = decode_pack_manifest(path).with_context(|| {
152        format!(
153            "failed to decode manifest.cbor (extensions are required) from {}",
154            path.display()
155        )
156    })?;
157    let mut registry = ProviderExtensionsRegistry::default();
158    apply_extensions(&mut registry, manifest.extensions.as_ref());
159    Ok(registry)
160}
161
162fn decode_pack_manifest(path: &Path) -> Result<PackManifest> {
163    let file = std::fs::File::open(path)?;
164    let mut archive = zip::ZipArchive::new(file)?;
165    let mut buf = Vec::new();
166    archive.by_name("manifest.cbor")?.read_to_end(&mut buf)?;
167    greentic_types::decode_pack_manifest(&buf).context("invalid pack manifest")
168}
169
170fn apply_extensions(
171    registry: &mut ProviderExtensionsRegistry,
172    extensions: Option<&BTreeMap<String, ExtensionRef>>,
173) {
174    let Some(extensions) = extensions else {
175        return;
176    };
177    if let Some(payload) = extract_ingress(extensions) {
178        registry.ingress.extend(payload);
179    }
180    if let Some(payload) = extract_oauth(extensions) {
181        registry.oauth.extend(payload);
182    }
183    if let Some(payload) = extract_subscriptions(extensions) {
184        registry.subscriptions.extend(payload);
185    }
186}
187
188fn extract_ingress(
189    extensions: &BTreeMap<String, ExtensionRef>,
190) -> Option<BTreeMap<String, IngressProviderDecl>> {
191    let entry = extensions.get(INGRESS_EXTENSION_ID)?;
192    let inline = entry.inline.as_ref()?;
193    let ExtensionInline::Other(value) = inline else {
194        return None;
195    };
196    let payload: IngressPayload = serde_json::from_value(value.clone()).ok()?;
197    Some(payload.providers)
198}
199
200fn extract_oauth(
201    extensions: &BTreeMap<String, ExtensionRef>,
202) -> Option<BTreeMap<String, OAuthProviderDecl>> {
203    let entry = extensions.get(OAUTH_EXTENSION_ID)?;
204    let inline = entry.inline.as_ref()?;
205    let ExtensionInline::Other(value) = inline else {
206        return None;
207    };
208    let payload: OAuthPayload = serde_json::from_value(value.clone()).ok()?;
209    Some(payload.providers)
210}
211
212fn extract_subscriptions(
213    extensions: &BTreeMap<String, ExtensionRef>,
214) -> Option<BTreeMap<String, SubscriptionsProviderDecl>> {
215    let entry = extensions.get(SUBSCRIPTIONS_EXTENSION_ID)?;
216    let inline = entry.inline.as_ref()?;
217    let ExtensionInline::Other(value) = inline else {
218        return None;
219    };
220    let payload: SubscriptionsPayload = serde_json::from_value(value.clone()).ok()?;
221    Some(payload.providers)
222}