Whitespace + import-grouping fixups in the 9 SDK modules touched by F-Q-002. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
4.7 KiB
Rust
134 lines
4.7 KiB
Rust
//! `secrets::` Rhai bridge — encrypted per-app secrets (v1.1.7).
|
|
//!
|
|
//! ```rhai
|
|
//! secrets::set("stripe_key", "sk_live_xxx");
|
|
//! secrets::set("oauth", #{ client_id: "abc", client_secret: "xyz" });
|
|
//! let key = secrets::get("stripe_key"); // value or ()
|
|
//! let removed = secrets::delete("stripe_key"); // bool
|
|
//! let page = secrets::list(#{ cursor: (), limit: 100 });
|
|
//! // page = #{ names: [...], next_cursor: () | "..." }
|
|
//! ```
|
|
//!
|
|
//! Collection-less (secrets are per-app, like pubsub topics) so there's
|
|
//! no `::collection(...)`. Values are any JSON-serializable Rhai value
|
|
//! (String/Map/Array/number/bool); a String round-trips back as a
|
|
//! String. `app_id` is derived from `cx.app_id` in the service — it
|
|
//! never appears in the script-side signature, preserving cross-app
|
|
//! isolation.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use picloud_shared::{SdkCallCx, SecretsListPage, Services};
|
|
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};
|
|
|
|
use super::bridge::{block_on, dynamic_to_json, json_to_dynamic};
|
|
|
|
pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
|
|
let svc = services.secrets.clone();
|
|
let mut module = Module::new();
|
|
|
|
// secrets::set(name, value) — overwrites if present.
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"set",
|
|
move |name: &str, value: Dynamic| -> Result<(), Box<EvalAltResult>> {
|
|
let json = dynamic_to_json(&value);
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("secrets", async move { svc.set(&cx, name, json).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
// secrets::get(name) — decoded value, or () if missing.
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"get",
|
|
move |name: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let opt = block_on("secrets", async move { svc.get(&cx, name).await })?;
|
|
Ok(opt.map_or(Dynamic::UNIT, json_to_dynamic))
|
|
},
|
|
);
|
|
}
|
|
|
|
// secrets::delete(name) — bool was-present.
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"delete",
|
|
move |name: &str| -> Result<bool, Box<EvalAltResult>> {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("secrets", async move { svc.delete(&cx, name).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
// secrets::list(#{ cursor, limit }) — names only, cursor-paginated.
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"list",
|
|
move |opts: Map| -> Result<Map, Box<EvalAltResult>> {
|
|
let (cursor, limit) = parse_list_opts(&opts)?;
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let page: SecretsListPage = block_on("secrets", async move {
|
|
svc.list(&cx, cursor.as_deref(), limit).await
|
|
})?;
|
|
Ok(list_page_to_map(page))
|
|
},
|
|
);
|
|
}
|
|
|
|
engine.register_static_module("secrets", module.into());
|
|
}
|
|
|
|
/// Pull `cursor` (string or `()`) and `limit` (int or `()`) out of the
|
|
/// options map. Unknown/extra keys are ignored.
|
|
fn parse_list_opts(opts: &Map) -> Result<(Option<String>, u32), Box<EvalAltResult>> {
|
|
let cursor = match opts.get("cursor") {
|
|
None => None,
|
|
Some(d) if d.is_unit() => None,
|
|
Some(d) if d.is_string() => Some(d.clone().into_string().unwrap_or_default()),
|
|
Some(_) => return Err(runtime_err("secrets::list: cursor must be a string or ()")),
|
|
};
|
|
let limit = match opts.get("limit") {
|
|
None => 0,
|
|
Some(d) if d.is_unit() => 0,
|
|
Some(d) => {
|
|
let n = d
|
|
.as_int()
|
|
.map_err(|_| runtime_err("secrets::list: limit must be an integer or ()"))?;
|
|
u32::try_from(n.max(0)).unwrap_or(u32::MAX)
|
|
}
|
|
};
|
|
Ok((cursor, limit))
|
|
}
|
|
|
|
fn list_page_to_map(page: SecretsListPage) -> Map {
|
|
let mut m = Map::new();
|
|
let names: Array = page.names.into_iter().map(Dynamic::from).collect();
|
|
m.insert("names".into(), names.into());
|
|
m.insert(
|
|
"next_cursor".into(),
|
|
page.next_cursor.map_or(Dynamic::UNIT, Dynamic::from),
|
|
);
|
|
m
|
|
}
|
|
|
|
// Returns the boxed error directly because every caller needs a
|
|
// `Box<EvalAltResult>` (Rhai's error type), matching the other bridges.
|
|
#[allow(clippy::unnecessary_box_returns)]
|
|
fn runtime_err(msg: &str) -> Box<EvalAltResult> {
|
|
EvalAltResult::ErrorRuntime(msg.into(), rhai::Position::NONE).into()
|
|
}
|