obzenflow-idkit 0.2.1

Phantom-typed ULID identifiers for ObzenFlow - type-safe IDs for native and WASM
Documentation
# ObzenFlow IDKit

ObzenFlow IDKit provides phantom-typed ULIDs for ObzenFlow (native server + Leptos wasm). Extracted from the main ObzenFlow project to provide type-safe IDs across the ecosystem.

* Type-only by default (no RNG dependencies).
* App crates enable `gen` to generate IDs (server + wasm).
* Optional `serde` support (serializes as a ULID string).
* Re-exports `ulid::Ulid` for interop.

## Why this exists

In a full-stack Rust app (native server + Leptos in the browser), you want one ID type that round-trips cleanly between back end, shared crates, and the frontend.

ULIDs are a good fit: they're 128-bit IDs (timestamp + randomness) that serialize to a compact, human-friendly string (useful for URLs and JSON). The server can generate them from OS entropy, but browser wasm can't access an OS RNG directly; it needs to go through Web APIs like `window.crypto.getRandomValues` (and Node-based wasm tests need `globalThis.crypto`).

This crate keeps that complexity out of shared code: it's **type-only by default**, while app crates opt into generation via `gen` and configure the RNG backend per target. See "Targets & RNG backends" below for specifics.

## ObzenFlow pattern

* Define marker types in domain crates (`struct User; type UserId = Id<User>;`); this crate stays generic.
* Shared crates should not enable `gen` (often `features = ["serde"]` only).
* App crates enable `gen` when they need to generate IDs (server or Leptos).
* Don't use `Id::default()` unless `gen` is enabled (apps only).
* Versioning follows SemVer; prior to 1.0, minor releases may include breaking changes.

## Install

**Server (native)**

```toml
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["gen", "serde"] }
```

**Leptos app (wasm, browser)**

```toml
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["gen", "serde"] }
# Route RNG to window.crypto.getRandomValues
getrandom = { version = "0.2", features = ["js"] }
```

**Shared crates (e.g., topology)**

```toml
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["serde"] } # no "gen" here
```

> Node-based wasm tests: set `globalThis.crypto = require('node:crypto').webcrypto`.

## Quick start

```rust
use obzenflow_idkit::{Id, Ulid};

pub struct User;
pub type UserId = Id<User>;

// Parse / round-trip (shared crates can do this without `gen`)
let from_wire: UserId = "01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap();
let ulid: Ulid = from_wire.as_ulid();

// Generate (apps with `features = ["gen"]`)
#[cfg(feature = "gen")]
let generated = UserId::new();
```

Common helpers: `from_ulid`, `as_ulid`, `to_bytes`/`from_bytes`, `timestamp_ms()`.

If you don't need phantom typing, you can generate raw ULIDs with `new_ulid()` / `new_ulid_string()` (requires `gen`).

## Testing (no RNG)

Keep `cargo test` trivial in shared crates by synthesizing IDs:

```rust
#[cfg(test)]
mod test_ids {
    use std::sync::atomic::{AtomicU128, Ordering};
    use obzenflow_idkit::Id;

    pub struct TestKind;
    static CTR: AtomicU128 = AtomicU128::new(0);

    pub fn next_id() -> Id<TestKind> {
        Id::from_bytes(CTR.fetch_add(1, Ordering::Relaxed).to_be_bytes())
    }
}
```

Use `next_id()` in tests, or provide constructors that accept IDs explicitly.

## Targets & RNG backends

* **Native:** OS entropy via `getrandom` (apps enable `gen`).
* **Browser wasm:** add `getrandom = { features = ["js"] }` in the **app**; `Id::new()` uses `crypto.getRandomValues`.
* **Node (wasm tests):** set `globalThis.crypto` as above.

## CI guardrails (recommended)

* Build both targets for shared crates:

  ```bash
  cargo build -p your_shared_crate
  cargo build -p your_shared_crate --target wasm32-unknown-unknown
  ```

* Fail if any shared crate calls `Id::new`/`new_ulid`/`new_ulid_string` outside tests:

  ```bash
  rg -n "(Id::new|new_ulid|new_ulid_string)\\s*\\(" crates/your_shared_crate -g '!**/*test*' && exit 1
  ```

## Project policies

* Security: `SECURITY.md`
* Code of Conduct: `CODE_OF_CONDUCT.md`
* Trademarks: `TRADEMARKS.md`

## License

Dual-licensed under MIT OR Apache-2.0.