1use std::collections::BTreeSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::Context;
6use serde::{Deserialize, Serialize};
7use serde_cbor::Value as CborValue;
8use zip::result::ZipError;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
11pub enum Domain {
12 Messaging,
13 Events,
14 Secrets,
15 OAuth,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum DomainAction {
20 Setup,
21 Diagnostics,
22 Verify,
23}
24
25#[derive(Clone, Debug)]
26pub struct DomainConfig {
27 pub providers_dir: &'static str,
28 pub setup_flow: &'static str,
29 pub diagnostics_flow: &'static str,
30 pub verify_flows: &'static [&'static str],
31}
32
33#[derive(Clone, Debug, Serialize)]
34pub struct ProviderPack {
35 pub pack_id: String,
36 pub file_name: String,
37 pub path: PathBuf,
38 pub entry_flows: Vec<String>,
39}
40
41#[derive(Clone, Debug, Serialize)]
42pub struct PlannedRun {
43 pub pack: ProviderPack,
44 pub flow_id: String,
45}
46
47pub fn config(domain: Domain) -> DomainConfig {
48 match domain {
49 Domain::Messaging => DomainConfig {
50 providers_dir: "providers/messaging",
51 setup_flow: "setup_default",
52 diagnostics_flow: "diagnostics",
53 verify_flows: &["verify_webhooks"],
54 },
55 Domain::Events => DomainConfig {
56 providers_dir: "providers/events",
57 setup_flow: "setup_default",
58 diagnostics_flow: "diagnostics",
59 verify_flows: &["verify_subscriptions"],
60 },
61 Domain::Secrets => DomainConfig {
62 providers_dir: "providers/secrets",
63 setup_flow: "setup_default",
64 diagnostics_flow: "diagnostics",
65 verify_flows: &[],
66 },
67 Domain::OAuth => DomainConfig {
68 providers_dir: "providers/oauth",
69 setup_flow: "setup_default",
70 diagnostics_flow: "diagnostics",
71 verify_flows: &[],
72 },
73 }
74}
75
76pub fn validator_pack_path(root: &Path, domain: Domain) -> Option<PathBuf> {
77 let name = match domain {
78 Domain::Messaging => "validators-messaging.gtpack",
79 Domain::Events => "validators-events.gtpack",
80 Domain::Secrets => "validators-secrets.gtpack",
81 Domain::OAuth => "validators-oauth.gtpack",
82 };
83 let path = root.join("validators").join(domain_name(domain)).join(name);
84 if path.exists() { Some(path) } else { None }
85}
86
87pub fn ensure_cbor_packs(root: &Path) -> anyhow::Result<()> {
88 let mut roots = Vec::new();
89 let providers = root.join("providers");
90 if providers.exists() {
91 roots.push(providers);
92 }
93 let packs = root.join("packs");
94 if packs.exists() {
95 roots.push(packs);
96 }
97 for root in roots {
98 for pack in collect_gtpacks(&root)? {
99 let file = std::fs::File::open(&pack)?;
100 let mut archive = zip::ZipArchive::new(file)?;
101 let manifest = read_manifest_cbor(&mut archive, &pack).map_err(|err| {
102 anyhow::anyhow!(
103 "failed to decode manifest.cbor in {}: {err}",
104 pack.display()
105 )
106 })?;
107 if manifest.is_none() {
108 return Err(missing_cbor_error(&pack));
109 }
110 }
111 }
112 Ok(())
113}
114
115pub fn manifest_cbor_issue_detail(path: &Path) -> anyhow::Result<Option<String>> {
116 let file = std::fs::File::open(path)?;
117 let mut archive = zip::ZipArchive::new(file)?;
118 let mut manifest = match archive.by_name("manifest.cbor") {
119 Ok(file) => file,
120 Err(ZipError::FileNotFound) => {
121 return Ok(Some("manifest.cbor missing from archive".to_string()));
122 }
123 Err(err) => return Err(err.into()),
124 };
125 let mut bytes = Vec::new();
126 std::io::Read::read_to_end(&mut manifest, &mut bytes)?;
127 let value = match serde_cbor::from_slice::<CborValue>(&bytes) {
128 Ok(value) => value,
129 Err(err) => return Ok(Some(err.to_string())),
130 };
131 if let Some(path) = find_manifest_string_type_mismatch(&value) {
132 return Ok(Some(format!("invalid type at {path} (expected string)")));
133 }
134 Ok(None)
135}
136
137fn collect_gtpacks(root: &Path) -> anyhow::Result<Vec<PathBuf>> {
138 let mut packs = Vec::new();
139 let mut stack = vec![root.to_path_buf()];
140 while let Some(dir) = stack.pop() {
141 for entry in std::fs::read_dir(&dir)? {
142 let entry = entry?;
143 let path = entry.path();
144 if entry.file_type()?.is_dir() {
145 stack.push(path);
146 continue;
147 }
148 if path.extension().and_then(|ext| ext.to_str()) == Some("gtpack") {
149 packs.push(path);
150 }
151 }
152 }
153 Ok(packs)
154}
155
156fn append_packs_from_root<F>(
157 packs: &mut Vec<ProviderPack>,
158 seen: &mut BTreeSet<PathBuf>,
159 root: &Path,
160 read_manifest: F,
161) -> anyhow::Result<()>
162where
163 F: Fn(&Path) -> anyhow::Result<PackManifest>,
164{
165 if !root.exists() {
166 return Ok(());
167 }
168 for path in collect_gtpacks(root)? {
169 append_pack(packs, seen, path, &read_manifest)?;
170 }
171 Ok(())
172}
173
174fn append_pack<F>(
175 packs: &mut Vec<ProviderPack>,
176 seen: &mut BTreeSet<PathBuf>,
177 path: PathBuf,
178 read_manifest: &F,
179) -> anyhow::Result<()>
180where
181 F: Fn(&Path) -> anyhow::Result<PackManifest>,
182{
183 if !seen.insert(path.clone()) {
184 return Ok(());
185 }
186 let file_name = path
187 .file_name()
188 .and_then(|value| value.to_str())
189 .ok_or_else(|| anyhow::anyhow!("invalid pack file name: {}", path.display()))?
190 .to_string();
191 let manifest = read_manifest(&path)?;
192 let meta = manifest
193 .meta
194 .ok_or_else(|| anyhow::anyhow!("pack manifest missing meta in {}", path.display()))?;
195 packs.push(ProviderPack {
196 pack_id: meta.pack_id,
197 file_name,
198 path,
199 entry_flows: meta.entry_flows,
200 });
201 Ok(())
202}
203
204pub fn discover_provider_packs(root: &Path, domain: Domain) -> anyhow::Result<Vec<ProviderPack>> {
205 let cfg = config(domain);
206 let providers_dir = root.join(cfg.providers_dir);
207 let packs_dir = root.join("packs");
208 let mut packs = Vec::new();
209 let mut seen = BTreeSet::new();
210 append_packs_from_root(&mut packs, &mut seen, &providers_dir, read_pack_manifest)?;
211 append_packs_from_root(&mut packs, &mut seen, &packs_dir, read_pack_manifest)?;
212 packs.sort_by(|a, b| a.file_name.cmp(&b.file_name));
213 Ok(packs)
214}
215
216pub fn discover_provider_packs_cbor_only(
217 root: &Path,
218 domain: Domain,
219) -> anyhow::Result<Vec<ProviderPack>> {
220 let cfg = config(domain);
221 let providers_dir = root.join(cfg.providers_dir);
222 let packs_dir = root.join("packs");
223 let mut packs = Vec::new();
224 let mut seen = BTreeSet::new();
225 append_packs_from_root(
226 &mut packs,
227 &mut seen,
228 &providers_dir,
229 read_pack_manifest_cbor_only,
230 )?;
231 append_packs_from_root(
232 &mut packs,
233 &mut seen,
234 &packs_dir,
235 read_pack_manifest_cbor_only,
236 )?;
237 packs.sort_by(|a, b| a.file_name.cmp(&b.file_name));
238 Ok(packs)
239}
240
241pub fn plan_runs(
242 domain: Domain,
243 action: DomainAction,
244 packs: &[ProviderPack],
245 provider_filter: Option<&str>,
246 allow_missing_setup: bool,
247) -> anyhow::Result<Vec<PlannedRun>> {
248 let cfg = config(domain);
249 let flows: Vec<&str> = match action {
250 DomainAction::Setup => vec![cfg.setup_flow],
251 DomainAction::Diagnostics => vec![cfg.diagnostics_flow],
252 DomainAction::Verify => cfg.verify_flows.to_vec(),
253 };
254
255 let mut plan = Vec::new();
256 for pack in packs {
257 if let Some(filter) = provider_filter {
258 let file_stem = pack
259 .file_name
260 .strip_suffix(".gtpack")
261 .unwrap_or(&pack.file_name);
262 let matches = pack.pack_id == filter
263 || pack.file_name == filter
264 || file_stem == filter
265 || pack.pack_id.contains(filter)
266 || pack.file_name.contains(filter)
267 || file_stem.contains(filter);
268 if !matches {
269 continue;
270 }
271 }
272
273 for flow in &flows {
274 let has_flow = pack.entry_flows.iter().any(|entry| entry == flow);
275 if !has_flow {
276 if action == DomainAction::Setup && !allow_missing_setup {
277 return Err(anyhow::anyhow!(
278 "Missing required flow '{}' in provider pack {}",
279 flow,
280 pack.file_name
281 ));
282 }
283 eprintln!(
284 "Warning: provider pack {} missing flow {}; skipping.",
285 pack.file_name, flow
286 );
287 continue;
288 }
289 plan.push(PlannedRun {
290 pack: pack.clone(),
291 flow_id: (*flow).to_string(),
292 });
293 }
294 }
295 Ok(plan)
296}
297
298#[derive(Debug, Deserialize)]
299struct PackManifest {
300 #[serde(default)]
301 meta: Option<PackMeta>,
302 #[serde(default)]
303 pack_id: Option<String>,
304 #[serde(default)]
305 flows: Vec<PackFlow>,
306}
307
308#[derive(Debug, Deserialize)]
309pub(crate) struct PackMeta {
310 pub pack_id: String,
311 #[serde(default)]
312 pub entry_flows: Vec<String>,
313}
314
315#[derive(Debug, Deserialize)]
316struct PackFlow {
317 id: String,
318 #[serde(default)]
319 entrypoints: Vec<String>,
320}
321
322fn read_pack_manifest(path: &Path) -> anyhow::Result<PackManifest> {
323 let file = std::fs::File::open(path)?;
324 match zip::ZipArchive::new(file) {
325 Ok(mut archive) => {
326 let manifest = read_pack_manifest_data(&mut archive, path)
327 .with_context(|| format!("failed to read pack manifest from {}", path.display()))?;
328 let meta = build_pack_meta(&manifest, path);
329 Ok(PackManifest {
330 meta: Some(meta),
331 pack_id: None,
332 flows: Vec::new(),
333 })
334 }
335 Err(_) => read_pack_manifest_from_tar(path),
336 }
337}
338
339pub(crate) fn read_pack_meta(path: &Path) -> anyhow::Result<PackMeta> {
340 let manifest = if path.is_dir() {
341 read_pack_manifest_from_dir(path)
342 } else {
343 read_pack_manifest(path)
344 }?;
345 manifest
346 .meta
347 .ok_or_else(|| anyhow::anyhow!("pack manifest missing meta in {}", path.display()))
348}
349
350fn read_pack_manifest_cbor_only(path: &Path) -> anyhow::Result<PackManifest> {
351 let file = std::fs::File::open(path)?;
352 match zip::ZipArchive::new(file) {
353 Ok(mut archive) => {
354 let manifest = match read_manifest_cbor(&mut archive, path).map_err(|err| {
355 anyhow::anyhow!(
356 "failed to decode manifest.cbor in {}: {err}",
357 path.display()
358 )
359 })? {
360 Some(manifest) => manifest,
361 None => return Err(missing_cbor_error(path)),
362 };
363 let meta = build_pack_meta(&manifest, path);
364 Ok(PackManifest {
365 meta: Some(meta),
366 pack_id: None,
367 flows: Vec::new(),
368 })
369 }
370 Err(_) => read_pack_manifest_from_tar(path),
371 }
372}
373
374fn read_pack_manifest_data(
375 archive: &mut zip::ZipArchive<std::fs::File>,
376 path: &Path,
377) -> anyhow::Result<PackManifest> {
378 match read_manifest_cbor(archive, path) {
379 Ok(Some(manifest)) => return Ok(manifest),
380 Ok(None) => {}
381 Err(err) => {
382 return Err(anyhow::anyhow!(
383 "failed to decode manifest.cbor in {}: {err}",
384 path.display()
385 ));
386 }
387 }
388 match read_manifest_json(archive, "pack.manifest.json") {
389 Ok(Some(manifest)) => return Ok(manifest),
390 Ok(None) => {}
391 Err(err) => {
392 return Err(anyhow::anyhow!(
393 "failed to decode pack.manifest.json in {}: {err}",
394 path.display()
395 ));
396 }
397 }
398 Err(anyhow::anyhow!(
399 "pack manifest not found in archive {} (expected manifest.cbor or pack.manifest.json)",
400 path.display()
401 ))
402}
403
404fn read_manifest_cbor(
405 archive: &mut zip::ZipArchive<std::fs::File>,
406 _path: &Path,
407) -> anyhow::Result<Option<PackManifest>> {
408 let mut file = match archive.by_name("manifest.cbor") {
409 Ok(file) => file,
410 Err(ZipError::FileNotFound) => return Ok(None),
411 Err(err) => return Err(err.into()),
412 };
413 let mut bytes = Vec::new();
414 std::io::Read::read_to_end(&mut file, &mut bytes)?;
415 let manifest = parse_manifest_cbor_bytes(&bytes)?;
416 Ok(Some(manifest))
417}
418
419fn read_pack_manifest_from_dir(path: &Path) -> anyhow::Result<PackManifest> {
420 let manifest_path = path.join("manifest.cbor");
421 if !manifest_path.exists() {
422 return Err(anyhow::anyhow!(
423 "pack manifest missing manifest.cbor in {}",
424 path.display()
425 ));
426 }
427 let bytes = fs::read(&manifest_path)?;
428 parse_manifest_cbor_bytes(&bytes)
429}
430
431fn read_pack_manifest_from_tar(path: &Path) -> anyhow::Result<PackManifest> {
432 let bytes = read_tar_entry_bytes(path, "manifest.cbor")?;
433 let manifest = parse_manifest_cbor_bytes(&bytes)?;
434 let meta = build_pack_meta(&manifest, path);
435 Ok(PackManifest {
436 meta: Some(meta),
437 pack_id: None,
438 flows: Vec::new(),
439 })
440}
441
442fn read_tar_entry_bytes(path: &Path, entry_name: &str) -> anyhow::Result<Vec<u8>> {
443 let file = std::fs::File::open(path)?;
444 let mut archive = tar::Archive::new(file);
445 for entry in archive.entries()? {
446 let mut entry = entry?;
447 if entry.path()?.as_ref() == Path::new(entry_name) {
448 let mut bytes = Vec::new();
449 std::io::Read::read_to_end(&mut entry, &mut bytes)?;
450 return Ok(bytes);
451 }
452 }
453 Err(anyhow::anyhow!(
454 "pack manifest not found in archive {} (expected {entry_name})",
455 path.display()
456 ))
457}
458
459fn parse_manifest_cbor_bytes(bytes: &[u8]) -> anyhow::Result<PackManifest> {
460 let value: CborValue = serde_cbor::from_slice(bytes)?;
461 match decode_manifest_lenient(&value) {
462 Ok(manifest) => Ok(manifest),
463 Err(decode_err) => {
464 if let Some(err_path) = find_manifest_string_type_mismatch(&value) {
465 return Err(anyhow::anyhow!(
466 "invalid type at {} (expected string)",
467 err_path
468 ));
469 }
470 Err(anyhow::anyhow!(
471 "manifest.cbor uses symbol table encoding but could not be decoded: {decode_err}"
472 ))
473 }
474 }
475}
476
477fn build_pack_meta(manifest: &PackManifest, path: &Path) -> PackMeta {
478 let pack_id = manifest
479 .meta
480 .as_ref()
481 .map(|meta| meta.pack_id.clone())
482 .or_else(|| manifest.pack_id.clone())
483 .unwrap_or_else(|| {
484 let fallback = path
485 .file_stem()
486 .and_then(|value| value.to_str())
487 .unwrap_or("pack")
488 .to_string();
489 eprintln!(
490 "Warning: pack manifest missing pack id; using filename '{}' for {}",
491 fallback,
492 path.display()
493 );
494 fallback
495 });
496 let mut entry_flows = manifest
497 .meta
498 .as_ref()
499 .map(|meta| meta.entry_flows.clone())
500 .unwrap_or_default();
501 if entry_flows.is_empty() {
502 for flow in &manifest.flows {
503 entry_flows.push(flow.id.clone());
504 entry_flows.extend(flow.entrypoints.iter().cloned());
505 }
506 }
507 if entry_flows.is_empty() {
508 entry_flows.push(pack_id.clone());
509 }
510 PackMeta {
511 pack_id,
512 entry_flows,
513 }
514}
515
516fn read_manifest_json(
517 archive: &mut zip::ZipArchive<std::fs::File>,
518 name: &str,
519) -> anyhow::Result<Option<PackManifest>> {
520 let mut file = match archive.by_name(name) {
521 Ok(file) => file,
522 Err(ZipError::FileNotFound) => return Ok(None),
523 Err(err) => return Err(err.into()),
524 };
525 let mut contents = String::new();
526 std::io::Read::read_to_string(&mut file, &mut contents)?;
527 let manifest: PackManifest = serde_json::from_str(&contents)?;
528 Ok(Some(manifest))
529}
530
531fn find_manifest_string_type_mismatch(value: &CborValue) -> Option<String> {
532 let CborValue::Map(map) = value else {
533 return None;
534 };
535 let symbols = symbols_map(map);
536
537 if let Some(pack_id) = map_get(map, "pack_id")
538 && !value_is_string_or_symbol(pack_id, symbols, "pack_ids")
539 {
540 return Some("pack_id".to_string());
541 }
542
543 if let Some(meta) = map_get(map, "meta") {
544 let CborValue::Map(meta_map) = meta else {
545 return Some("meta".to_string());
546 };
547 if let Some(pack_id) = map_get(meta_map, "pack_id")
548 && !value_is_string_or_symbol(pack_id, symbols, "pack_ids")
549 {
550 return Some("meta.pack_id".to_string());
551 }
552 if let Some(entry_flows) = map_get(meta_map, "entry_flows") {
553 let CborValue::Array(values) = entry_flows else {
554 return Some("meta.entry_flows".to_string());
555 };
556 for (idx, value) in values.iter().enumerate() {
557 if !value_is_string_or_symbol(value, symbols, "flow_ids") {
558 return Some(format!("meta.entry_flows[{idx}]"));
559 }
560 }
561 }
562 }
563
564 if let Some(flows) = map_get(map, "flows") {
565 let CborValue::Array(values) = flows else {
566 return Some("flows".to_string());
567 };
568 for (idx, value) in values.iter().enumerate() {
569 let CborValue::Map(flow) = value else {
570 return Some(format!("flows[{idx}]"));
571 };
572 if let Some(id) = map_get(flow, "id")
573 && !value_is_string_or_symbol(id, symbols, "flow_ids")
574 {
575 return Some(format!("flows[{idx}].id"));
576 }
577 if let Some(entrypoints) = map_get(flow, "entrypoints") {
578 let CborValue::Array(values) = entrypoints else {
579 return Some(format!("flows[{idx}].entrypoints"));
580 };
581 for (jdx, value) in values.iter().enumerate() {
582 if !value_is_string_or_symbol(value, symbols, "entrypoints") {
583 return Some(format!("flows[{idx}].entrypoints[{jdx}]"));
584 }
585 }
586 }
587 }
588 }
589
590 None
591}
592
593fn map_get<'a>(
594 map: &'a std::collections::BTreeMap<CborValue, CborValue>,
595 key: &str,
596) -> Option<&'a CborValue> {
597 map.iter().find_map(|(k, v)| match k {
598 CborValue::Text(text) if text == key => Some(v),
599 _ => None,
600 })
601}
602
603fn symbols_map(
604 map: &std::collections::BTreeMap<CborValue, CborValue>,
605) -> Option<&std::collections::BTreeMap<CborValue, CborValue>> {
606 let symbols = map_get(map, "symbols")?;
607 match symbols {
608 CborValue::Map(map) => Some(map),
609 _ => None,
610 }
611}
612
613fn value_is_string_or_symbol(
614 value: &CborValue,
615 symbols: Option<&std::collections::BTreeMap<CborValue, CborValue>>,
616 symbol_key: &str,
617) -> bool {
618 if matches!(value, CborValue::Text(_)) {
619 return true;
620 }
621 let CborValue::Integer(idx) = value else {
622 return false;
623 };
624 let symbols = match symbols {
625 Some(symbols) => symbols,
626 None => return true,
627 };
628 let Some(CborValue::Array(values)) = map_get(symbols, symbol_key)
629 .or_else(|| map_get(symbols, symbol_key.strip_suffix('s').unwrap_or(symbol_key)))
630 else {
631 return true;
632 };
633 let idx = match usize::try_from(*idx) {
634 Ok(idx) => idx,
635 Err(_) => return true,
636 };
637 matches!(values.get(idx), Some(CborValue::Text(_)))
638}
639
640fn decode_manifest_lenient(value: &CborValue) -> anyhow::Result<PackManifest> {
641 let CborValue::Map(map) = value else {
642 return Err(anyhow::anyhow!("manifest is not a map"));
643 };
644 let symbols = symbols_map(map);
645
646 let (meta_pack_id, meta_entry_flows) = if let Some(meta) = map_get(map, "meta") {
647 let CborValue::Map(meta_map) = meta else {
648 return Err(anyhow::anyhow!("meta is not a map"));
649 };
650 let pack_id = resolve_string_symbol(map_get(meta_map, "pack_id"), symbols, "pack_ids")?;
651 let entry_flows = resolve_string_array(
652 map_get(meta_map, "entry_flows"),
653 symbols,
654 "flow_ids",
655 Some("entrypoints"),
656 )?;
657 (pack_id, entry_flows)
658 } else {
659 (None, Vec::new())
660 };
661
662 let pack_id = resolve_string_symbol(map_get(map, "pack_id"), symbols, "pack_ids")?
663 .or(meta_pack_id)
664 .ok_or_else(|| anyhow::anyhow!("pack_id missing"))?;
665
666 let mut flows = Vec::new();
667 if let Some(flows_value) = map_get(map, "flows") {
668 let CborValue::Array(values) = flows_value else {
669 return Err(anyhow::anyhow!("flows is not an array"));
670 };
671 for (idx, value) in values.iter().enumerate() {
672 let CborValue::Map(flow) = value else {
673 return Err(anyhow::anyhow!("flows[{idx}] is not a map"));
674 };
675 let id = resolve_string_symbol(map_get(flow, "id"), symbols, "flow_ids")?
676 .ok_or_else(|| anyhow::anyhow!("flows[{idx}].id missing"))?;
677 let entrypoints =
678 resolve_string_array(map_get(flow, "entrypoints"), symbols, "entrypoints", None)?;
679 flows.push(PackFlow { id, entrypoints });
680 }
681 }
682
683 Ok(PackManifest {
684 meta: Some(PackMeta {
685 pack_id,
686 entry_flows: meta_entry_flows,
687 }),
688 pack_id: None,
689 flows,
690 })
691}
692
693fn resolve_string_symbol(
694 value: Option<&CborValue>,
695 symbols: Option<&std::collections::BTreeMap<CborValue, CborValue>>,
696 symbol_key: &str,
697) -> anyhow::Result<Option<String>> {
698 let Some(value) = value else {
699 return Ok(None);
700 };
701 match value {
702 CborValue::Text(text) => Ok(Some(text.clone())),
703 CborValue::Integer(idx) => {
704 let Some(symbols) = symbols else {
705 return Ok(Some(idx.to_string()));
706 };
707 let Some(values) = symbol_array(symbols, symbol_key) else {
708 return Ok(Some(idx.to_string()));
709 };
710 let idx = usize::try_from(*idx).unwrap_or(usize::MAX);
711 match values.get(idx) {
712 Some(CborValue::Text(text)) => Ok(Some(text.clone())),
713 _ => Ok(Some(idx.to_string())),
714 }
715 }
716 _ => Err(anyhow::anyhow!("expected string or symbol index")),
717 }
718}
719
720fn symbol_array<'a>(
721 symbols: &'a std::collections::BTreeMap<CborValue, CborValue>,
722 key: &'a str,
723) -> Option<&'a Vec<CborValue>> {
724 if let Some(CborValue::Array(values)) = map_get(symbols, key) {
725 return Some(values);
726 }
727 if let Some(stripped) = key.strip_suffix('s')
728 && let Some(CborValue::Array(values)) = map_get(symbols, stripped)
729 {
730 return Some(values);
731 }
732 None
733}
734
735fn resolve_string_array(
736 value: Option<&CborValue>,
737 symbols: Option<&std::collections::BTreeMap<CborValue, CborValue>>,
738 symbol_key: &str,
739 fallback_key: Option<&str>,
740) -> anyhow::Result<Vec<String>> {
741 let Some(value) = value else {
742 return Ok(Vec::new());
743 };
744 let CborValue::Array(values) = value else {
745 return Err(anyhow::anyhow!("expected array"));
746 };
747 let mut out = Vec::new();
748 for (idx, value) in values.iter().enumerate() {
749 match resolve_string_symbol(Some(value), symbols, symbol_key) {
750 Ok(Some(value)) => out.push(value),
751 Ok(None) => {}
752 Err(err) => {
753 if let Some(fallback_key) = fallback_key
754 && let Ok(Some(value)) =
755 resolve_string_symbol(Some(value), symbols, fallback_key)
756 {
757 out.push(value);
758 continue;
759 }
760 return Err(anyhow::anyhow!("{err} at index {idx}"));
761 }
762 }
763 }
764 Ok(out)
765}
766
767fn missing_cbor_error(path: &Path) -> anyhow::Error {
768 anyhow::anyhow!(
769 "ERROR: demo packs must be CBOR-only (.gtpack must contain manifest.cbor). Rebuild the pack with greentic-pack build (do not use --dev). Missing in {}",
770 path.display()
771 )
772}
773
774pub(crate) fn domain_name(domain: Domain) -> &'static str {
775 match domain {
776 Domain::Messaging => "messaging",
777 Domain::Events => "events",
778 Domain::Secrets => "secrets",
779 Domain::OAuth => "oauth",
780 }
781}