open-standards/jcp/jcp-rs/crates/jcp-bridge/examples/mint_token.rs
Transaction Science 22e4af3158 jcp: caveats — the open attenuation axis (SPEC §4.9, C24)
Attenuation's axes all narrow by set-subtraction: remove actions, lower a
ceiling, shorten an expiry. They cannot express a condition the issuer did not
anticipate. `jcp-policy` can, but it is service-side — evaluated against a
PolicyContext the verifier must be told about, not carried in the capability.

A caveat is a condition embedded INSIDE the signed grant. Any enforcement point
holding it checks offline: no call back to the issuer, no shared policy config.
That is the distinction — policy is something a verifier must be told; a caveat
is something the token carries.

A design constraint found in the code rather than assumed: the predicate set is
bounded by what `decide` actually holds. jcp-policy's `Match::ContentContains`
CANNOT become a caveat, because the runtime sees a frame's `payload_hash` and
never its bytes. So a condition on content is expressible only as `PayloadIs`,
binding an exact payload rather than inspecting one. SPEC §4.9 states that
constraint normatively so implementations don't invent undecidable predicates.

Two variants express authority the fixed axes cannot:
- `PayloadIs` makes a grant ONE-SHOT — it authorizes one specific call, so a
  leaked grant cannot be replayed against different arguments. `capability`
  narrows to an action; it can never narrow to a call.
- `MaxCostUj` bounds a SINGLE invocation, where `budget_uj` is cumulative — so a
  delegate cannot burn the whole remaining budget in one call.
Plus ActionIn (may narrow below the capability), CallerIs, NotBefore, NotAfter.

Enforcement is in decide/decide_priced with the real cost. The two-phase
`authorize` path skips only the cost-dependent caveat — the cost is unknowable
there — and enforces every other; that binds at decide time and is documented
rather than silently ignored.

Attenuation is monotone: `attenuate` INHERITS the parent's caveats (dropping
them would widen a delegate's authority — the same amplification the dual-ceiling
work had to guard), `attenuate_caveated` appends, and `caveats_narrow` refuses a
child that lost one. Adding the field with no default made the compiler flag
every Grant construction site, which is how three bad auto-patches into function
parameter lists surfaced as build errors instead of silent breakage.

Empty caveat set ⇒ omitted from the canonical encoding, so a grant without
caveats is byte-identical to one minted before they existed — content id and
signature unchanged, asserted by test and by C24.

42 suites green, clippy clean, all conformance checks pass including C24.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-19 11:03:04 -04:00

31 lines
1.2 KiB
Rust

//! Mint a real JCP-grant-backed OAuth access token and print it + the issuer
//! public key, so an independent verifier (any RFC 7515/9068 JWT stack) can
//! check it. Used to prove the binding interoperates outside Rust.
use ed25519_dalek::SigningKey;
use jcp_bridge::oauth::grant_to_access_token;
use jcp_grant::issue;
use jcp_schema::{Capability, ContentId, Did, Grant};
fn main() {
let sk = SigningKey::from_bytes(&[9u8; 32]);
let issuer = Did::from_verifying_key(&sk.verifying_key());
let subject = Did::from_verifying_key(&SigningKey::from_bytes(&[2u8; 32]).verifying_key());
let grant = Grant {
issuer,
subject,
capability: Capability::new(ContentId::of_bytes(b"resource"), ["read", "write"]),
budget_uj: 5_000_000,
budget_funds: None,
caveats: Vec::new(),
expiry: 0,
parent: None,
delegate_depth: Some(3),
nonce: [7u8; 16],
};
let sg = issue(&sk, grant).expect("issue");
let token = grant_to_access_token(&sg, &sk, 1_000_000, 3600).expect("mint");
let hex: String = sk.verifying_key().to_bytes().iter().map(|b| format!("{b:02x}")).collect();
println!("TOKEN {token}");
println!("PUBKEY {hex}");
}