1use std::collections::BTreeSet;
2use std::fmt;
3use std::time::Duration;
4
5use crate::error::WasmModelError;
6use crate::ids::ExtensionPointKind;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub enum StorageClassGrant {
10 PublicUpload,
11 PrivateShared,
12 LocalOnlySensitive,
13 PublicAsset,
14}
15
16impl fmt::Display for StorageClassGrant {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::PublicUpload => f.write_str("public_upload"),
20 Self::PrivateShared => f.write_str("private_shared"),
21 Self::LocalOnlySensitive => f.write_str("local_only_sensitive"),
22 Self::PublicAsset => f.write_str("public_asset"),
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub enum MetadataGrant {
29 JsonLd,
30 SitemapEntry,
31 Translation,
32 SeoHead,
33}
34
35impl fmt::Display for MetadataGrant {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::JsonLd => f.write_str("json_ld"),
39 Self::SitemapEntry => f.write_str("sitemap_entry"),
40 Self::Translation => f.write_str("translation"),
41 Self::SeoHead => f.write_str("seo_head"),
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub enum HostCapabilityGrant {
48 DataRead { resource: String },
49 DataWrite { resource: String },
50 AuthCheck,
51 AuthList,
52 AuthLookup,
53 AuthTupleWrite,
54 StorageRead { class: StorageClassGrant },
55 StorageWrite { class: StorageClassGrant },
56 RenderFragment { slot: String },
57 MetadataWrite { kind: MetadataGrant },
58 CacheHintWrite,
59 OutboundHttp { integration: String },
60 SecretRead { secret: String },
61 EnqueueJob { queue: String },
62}
63
64impl fmt::Display for HostCapabilityGrant {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::DataRead { resource } => write!(f, "data.read:{resource}"),
68 Self::DataWrite { resource } => write!(f, "data.write:{resource}"),
69 Self::AuthCheck => f.write_str("auth.check"),
70 Self::AuthList => f.write_str("auth.list"),
71 Self::AuthLookup => f.write_str("auth.lookup"),
72 Self::AuthTupleWrite => f.write_str("auth.tuple_write"),
73 Self::StorageRead { class } => write!(f, "storage.read:{class}"),
74 Self::StorageWrite { class } => write!(f, "storage.write:{class}"),
75 Self::RenderFragment { slot } => write!(f, "render.fragment:{slot}"),
76 Self::MetadataWrite { kind } => write!(f, "metadata.write:{kind}"),
77 Self::CacheHintWrite => f.write_str("cache.hint.write"),
78 Self::OutboundHttp { integration } => write!(f, "http.outbound:{integration}"),
79 Self::SecretRead { secret } => write!(f, "secret.read:{secret}"),
80 Self::EnqueueJob { queue } => write!(f, "job.enqueue:{queue}"),
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Default)]
86pub struct HostGrantSet {
87 grants: BTreeSet<HostCapabilityGrant>,
88}
89
90impl HostGrantSet {
91 pub fn new() -> Self {
92 Self::default()
93 }
94
95 pub fn from_grants(grants: impl IntoIterator<Item = HostCapabilityGrant>) -> Self {
96 let mut set = Self::new();
97 for grant in grants {
98 set.insert(grant);
99 }
100 set
101 }
102
103 pub fn insert(&mut self, grant: HostCapabilityGrant) {
104 self.grants.insert(grant);
105 }
106
107 pub fn contains(&self, grant: &HostCapabilityGrant) -> bool {
108 self.grants.contains(grant)
109 }
110
111 pub fn is_subset_of(&self, other: &Self) -> bool {
112 self.grants.iter().all(|grant| other.contains(grant))
113 }
114
115 pub fn len(&self) -> usize {
116 self.grants.len()
117 }
118
119 pub fn iter(&self) -> impl Iterator<Item = &HostCapabilityGrant> {
120 self.grants.iter()
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct ResourceLimits {
126 pub max_runtime: Duration,
127 pub max_memory_bytes: u64,
128 pub max_outbound_requests: u32,
129 pub max_outbound_response_bytes: u64,
130 pub max_storage_writes: u32,
131 pub max_storage_bytes: u64,
132 pub max_concurrency: u16,
133}
134
135impl ResourceLimits {
136 pub const fn new(
137 max_runtime: Duration,
138 max_memory_bytes: u64,
139 max_outbound_requests: u32,
140 max_outbound_response_bytes: u64,
141 max_storage_writes: u32,
142 max_storage_bytes: u64,
143 max_concurrency: u16,
144 ) -> Self {
145 Self {
146 max_runtime,
147 max_memory_bytes,
148 max_outbound_requests,
149 max_outbound_response_bytes,
150 max_storage_writes,
151 max_storage_bytes,
152 max_concurrency,
153 }
154 }
155
156 pub fn baseline_for(point: ExtensionPointKind) -> Self {
157 match point {
158 ExtensionPointKind::Page
159 | ExtensionPointKind::Api
160 | ExtensionPointKind::AdminWidget
161 | ExtensionPointKind::RenderHook => Self::new(
162 Duration::from_secs(2),
163 64 * 1024 * 1024,
164 4,
165 4 * 1024 * 1024,
166 2,
167 8 * 1024 * 1024,
168 32,
169 ),
170 ExtensionPointKind::Webhook => Self::new(
171 Duration::from_secs(5),
172 64 * 1024 * 1024,
173 6,
174 8 * 1024 * 1024,
175 2,
176 8 * 1024 * 1024,
177 16,
178 ),
179 ExtensionPointKind::Job | ExtensionPointKind::ScheduledJob => Self::new(
180 Duration::from_secs(30),
181 128 * 1024 * 1024,
182 20,
183 16 * 1024 * 1024,
184 16,
185 64 * 1024 * 1024,
186 4,
187 ),
188 }
189 }
190
191 pub fn validate(&self) -> Result<(), WasmModelError> {
192 if self.max_runtime.is_zero() {
193 return Err(WasmModelError::ZeroLimit {
194 field: "max_runtime",
195 });
196 }
197 if self.max_memory_bytes == 0 {
198 return Err(WasmModelError::ZeroLimit {
199 field: "max_memory_bytes",
200 });
201 }
202 if self.max_outbound_requests == 0 {
203 return Err(WasmModelError::ZeroLimit {
204 field: "max_outbound_requests",
205 });
206 }
207 if self.max_outbound_response_bytes == 0 {
208 return Err(WasmModelError::ZeroLimit {
209 field: "max_outbound_response_bytes",
210 });
211 }
212 if self.max_storage_writes == 0 {
213 return Err(WasmModelError::ZeroLimit {
214 field: "max_storage_writes",
215 });
216 }
217 if self.max_storage_bytes == 0 {
218 return Err(WasmModelError::ZeroLimit {
219 field: "max_storage_bytes",
220 });
221 }
222 if self.max_concurrency == 0 {
223 return Err(WasmModelError::ZeroLimit {
224 field: "max_concurrency",
225 });
226 }
227
228 Ok(())
229 }
230
231 pub(crate) fn ensure_no_looser_than(
232 &self,
233 declared: &Self,
234 handler_id: &crate::ids::HandlerId,
235 ) -> Result<(), WasmModelError> {
236 let checks = [
237 (self.max_runtime <= declared.max_runtime, "max_runtime"),
238 (
239 self.max_memory_bytes <= declared.max_memory_bytes,
240 "max_memory_bytes",
241 ),
242 (
243 self.max_outbound_requests <= declared.max_outbound_requests,
244 "max_outbound_requests",
245 ),
246 (
247 self.max_outbound_response_bytes <= declared.max_outbound_response_bytes,
248 "max_outbound_response_bytes",
249 ),
250 (
251 self.max_storage_writes <= declared.max_storage_writes,
252 "max_storage_writes",
253 ),
254 (
255 self.max_storage_bytes <= declared.max_storage_bytes,
256 "max_storage_bytes",
257 ),
258 (
259 self.max_concurrency <= declared.max_concurrency,
260 "max_concurrency",
261 ),
262 ];
263
264 for (passes, field) in checks {
265 if !passes {
266 return Err(WasmModelError::LimitOverrideExceedsDeclared {
267 handler_id: handler_id.to_string(),
268 field,
269 });
270 }
271 }
272
273 Ok(())
274 }
275}