open-standards/openpay/crates/op-wasm/src/policy.rs
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

229 lines
6.9 KiB
Rust

//! JS-exposed tokenization policy.
//!
//! wasm-bindgen has limited support for compound types across the
//! boundary (no `Option<u64>`, no nested structs by-value). The
//! idiomatic shape is a class with a constructor + getters/setters.
//! That's what we use here.
//!
//! Two convenience static factories — `singleUse(ttl)` and
//! `cardOnFile()` — cover the two most common configurations and
//! mirror the same names used on iOS and Android.
use wasm_bindgen::prelude::*;
/// Tokenization format.
///
/// wasm-bindgen exports Rust enums as TypeScript-friendly union
/// constants on the JS side: `TokenFormat.Random` and
/// `TokenFormat.Deterministic`.
#[wasm_bindgen]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TokenFormat {
/// Fresh random token each call. PCI-aligned default.
Random = 0,
/// Same PAN → same token. Creates a query oracle.
Deterministic = 1,
}
/// Token lifetime.
#[wasm_bindgen]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TokenLifetime {
/// Token survives until explicitly deleted or expired.
Reusable = 0,
/// Consumed on first successful detokenize.
SingleUse = 1,
}
/// Tokenization policy.
///
/// The fields are not directly settable as JS struct fields because
/// wasm-bindgen serializes them through getters/setters; equivalent
/// shape but works across the boundary.
#[wasm_bindgen]
#[derive(Copy, Clone, Debug)]
pub struct TokenizationPolicy {
format: TokenFormat,
lifetime: TokenLifetime,
/// TTL in seconds. Zero means "no TTL".
///
/// We use `0` as the sentinel rather than null because
/// wasm-bindgen doesn't model `Option<u64>` directly without
/// boxing. The op-vault crate's TokenizationPolicy uses
/// `Option<u64>`; we translate at the boundary.
ttl_seconds: u64,
}
#[wasm_bindgen]
impl TokenizationPolicy {
/// Default: random format, reusable lifetime, no TTL.
#[wasm_bindgen(constructor)]
pub fn new() -> TokenizationPolicy {
TokenizationPolicy {
format: TokenFormat::Random,
lifetime: TokenLifetime::Reusable,
ttl_seconds: 0,
}
}
/// Short-lived single-use token. Appropriate for 3DS
/// authentication and other one-shot flows. Default TTL 120s.
#[wasm_bindgen(js_name = "singleUse")]
pub fn single_use(ttl_seconds: Option<u64>) -> TokenizationPolicy {
TokenizationPolicy {
format: TokenFormat::Random,
lifetime: TokenLifetime::SingleUse,
ttl_seconds: ttl_seconds.unwrap_or(120),
}
}
/// Long-lived random reusable token for card-on-file.
#[wasm_bindgen(js_name = "cardOnFile")]
pub fn card_on_file() -> TokenizationPolicy {
TokenizationPolicy {
format: TokenFormat::Random,
lifetime: TokenLifetime::Reusable,
ttl_seconds: 0,
}
}
/// Format getter.
#[wasm_bindgen(getter)]
pub fn format(&self) -> TokenFormat {
self.format
}
/// Format setter.
#[wasm_bindgen(setter)]
pub fn set_format(&mut self, v: TokenFormat) {
self.format = v;
}
/// Lifetime getter.
#[wasm_bindgen(getter)]
pub fn lifetime(&self) -> TokenLifetime {
self.lifetime
}
/// Lifetime setter.
#[wasm_bindgen(setter)]
pub fn set_lifetime(&mut self, v: TokenLifetime) {
self.lifetime = v;
}
/// TTL in seconds. `0` means "no TTL".
#[wasm_bindgen(getter, js_name = "ttlSeconds")]
pub fn ttl_seconds(&self) -> u64 {
self.ttl_seconds
}
/// TTL setter. Pass `0` for "no TTL".
#[wasm_bindgen(setter, js_name = "ttlSeconds")]
pub fn set_ttl_seconds(&mut self, v: u64) {
self.ttl_seconds = v;
}
}
impl Default for TokenizationPolicy {
fn default() -> Self {
Self::new()
}
}
impl TokenizationPolicy {
/// Internal: lower to the op_vault representation.
pub(crate) fn to_inner(self) -> op_vault::TokenizationPolicy {
op_vault::TokenizationPolicy {
format: match self.format {
TokenFormat::Random => op_vault::TokenFormat::Random,
TokenFormat::Deterministic => op_vault::TokenFormat::Deterministic,
},
lifetime: match self.lifetime {
TokenLifetime::Reusable => op_vault::TokenLifetime::Reusable,
TokenLifetime::SingleUse => op_vault::TokenLifetime::SingleUse,
},
ttl_seconds: if self.ttl_seconds == 0 {
None
} else {
Some(self.ttl_seconds)
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_random_reusable_no_ttl() {
let p = TokenizationPolicy::new();
assert_eq!(p.format(), TokenFormat::Random);
assert_eq!(p.lifetime(), TokenLifetime::Reusable);
assert_eq!(p.ttl_seconds(), 0);
}
#[test]
fn single_use_default_ttl_is_120() {
let p = TokenizationPolicy::single_use(None);
assert_eq!(p.lifetime(), TokenLifetime::SingleUse);
assert_eq!(p.ttl_seconds(), 120);
}
#[test]
fn single_use_with_explicit_ttl() {
let p = TokenizationPolicy::single_use(Some(60));
assert_eq!(p.ttl_seconds(), 60);
}
#[test]
fn card_on_file_is_reusable_no_ttl() {
let p = TokenizationPolicy::card_on_file();
assert_eq!(p.lifetime(), TokenLifetime::Reusable);
assert_eq!(p.ttl_seconds(), 0);
}
#[test]
fn setters_round_trip() {
let mut p = TokenizationPolicy::new();
p.set_format(TokenFormat::Deterministic);
p.set_lifetime(TokenLifetime::SingleUse);
p.set_ttl_seconds(45);
assert_eq!(p.format(), TokenFormat::Deterministic);
assert_eq!(p.lifetime(), TokenLifetime::SingleUse);
assert_eq!(p.ttl_seconds(), 45);
}
#[test]
fn to_inner_zero_ttl_maps_to_none() {
let p = TokenizationPolicy::new();
let inner = p.to_inner();
assert_eq!(inner.ttl_seconds, None);
}
#[test]
fn to_inner_nonzero_ttl_maps_to_some() {
let mut p = TokenizationPolicy::new();
p.set_ttl_seconds(120);
let inner = p.to_inner();
assert_eq!(inner.ttl_seconds, Some(120));
}
#[test]
fn to_inner_format_mapping() {
let mut p = TokenizationPolicy::new();
p.set_format(TokenFormat::Random);
assert_eq!(p.to_inner().format, op_vault::TokenFormat::Random);
p.set_format(TokenFormat::Deterministic);
assert_eq!(p.to_inner().format, op_vault::TokenFormat::Deterministic);
}
#[test]
fn to_inner_lifetime_mapping() {
let mut p = TokenizationPolicy::new();
p.set_lifetime(TokenLifetime::Reusable);
assert_eq!(p.to_inner().lifetime, op_vault::TokenLifetime::Reusable);
p.set_lifetime(TokenLifetime::SingleUse);
assert_eq!(p.to_inner().lifetime, op_vault::TokenLifetime::SingleUse);
}
}