bynk_check/actors.rs
1//! v0.45 actor contracts (the actors-foundations slice).
2//!
3//! An `actor` declaration is a nominal *boundary contract* (ADR Q1): a closed,
4//! compiler-known authentication `Scheme` plus an optional sealed identity. A
5//! handler consumes an actor on its `by` clause; the boundary verifies the
6//! scheme and mints the identity before the body runs (two-phase, fail-closed —
7//! ADR Q5/Q2).
8//!
9//! This module holds the compiler-known parts: the closed scheme set, the
10//! prelude actors, the per-protocol default actors, and the admissible-scheme
11//! sets. Foundations admits only the two zero-crypto schemes (`None`,
12//! `Internal`); `Bearer`/`Signature` are reserved-and-rejected.
13
14use std::collections::HashMap;
15
16use bynk_syntax::ast::{
17 BinOp, ByClause, Expr, ExprKind, Handler, HandlerKind, ServiceProtocol, TypeRef, UnaryOp,
18};
19use bynk_syntax::span::Span;
20
21/// P6.49 (design/tracks/the-ir.md §6b), following P6.27's `ExprId` precedent:
22/// every reader in this module — [`bearer_seam_for`], [`oidc_seam_for`],
23/// [`signature_seam_for`], [`sum_members_for`], [`caller_binder_for`] — is
24/// already parameterised by `&HashMap<String, ActorDecl>`. `bynk-emit` clones
25/// and forwards this table (`project.rs`'s `EmitProjectCtx::actors`,
26/// `emitter/workers.rs`/`workers_entry.rs`'s own `actors` parameters) without
27/// ever reading an `ActorDecl` field itself — P6.34 investigated and declined
28/// resolving it earlier, since `bynk-emit`'s own `lower_actor_seam_ir` needs
29/// the raw declarations to do its own resolution; this re-export changes the
30/// import path, not that data flow.
31pub use bynk_syntax::ast::ActorDecl;
32
33/// The authentication scheme — a closed, compiler-known set (ADR Q1). Sealed
34/// now, openable later by widening this enum.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Scheme {
37 /// Anonymous — no verification; identity is `()`. (`Visitor`.)
38 None,
39 /// In-system / platform trust — the channel itself is the assertion
40 /// (service-binding / platform dispatch). Admitted in Foundations.
41 Internal,
42 /// Bearer token — compiler-generated JWT/HS256 verification (ADR 0085).
43 Bearer,
44 /// Request signature — HMAC-SHA256 over the body (ADR 0089).
45 Signature,
46 /// OIDC/JWKS — compiler-generated verification of an asymmetrically-signed
47 /// (RS256/ES256) JWT against a provider's published JWKS, checking
48 /// `iss`/`aud`/`exp`/`nbf` and minting the identity from `sub` (ADR 0175).
49 /// The first scheme opened via the "user-configured verifier" route: the
50 /// trust root is the provider's public key set (a URL), so the declaration
51 /// carries **no inline secret** — only public trust parameters.
52 Oidc,
53}
54
55impl Scheme {
56 /// Classify a scheme name written in `auth = <Scheme>`. `None` means the
57 /// name is not one of the compiler-known schemes.
58 pub fn from_name(s: &str) -> Option<Scheme> {
59 Some(match s {
60 "None" => Scheme::None,
61 "Internal" => Scheme::Internal,
62 "Bearer" => Scheme::Bearer,
63 "Signature" => Scheme::Signature,
64 "Oidc" => Scheme::Oidc,
65 _ => return None,
66 })
67 }
68
69 /// The schemes the compiler can emit verification for. v0.45 admitted the
70 /// two zero-crypto schemes (`None`/`Internal`); v0.47 added `Bearer`
71 /// (JWT/HS256); v0.51 added `Signature` (HMAC over the body); v0.151 adds
72 /// `Oidc` (JWKS/RS256+ES256). All five schemes are now admitted.
73 pub fn admitted(self) -> bool {
74 matches!(
75 self,
76 Scheme::None | Scheme::Internal | Scheme::Bearer | Scheme::Signature | Scheme::Oidc
77 )
78 }
79
80 pub fn as_str(self) -> &'static str {
81 match self {
82 Scheme::None => "None",
83 Scheme::Internal => "Internal",
84 Scheme::Bearer => "Bearer",
85 Scheme::Signature => "Signature",
86 Scheme::Oidc => "Oidc",
87 }
88 }
89}
90
91/// The identity a verified actor yields (ADR Q2). In Foundations this is `()`
92/// for trivial actors, the built-in sealed `CallerId` for the cross-context
93/// `Internal` channel (Q7, folded in), or a context-owned declared type.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum Identity {
96 /// `()` — `None` actors and platform-tag `Internal` actors.
97 Unit,
98 /// The built-in sealed calling-context identity (Q7). Minted at the
99 /// service-binding seam; read-only and never re-checked.
100 CallerId,
101 /// A context-owned declared type named in `identity = <T>`.
102 Declared(String),
103}
104
105/// The built-in sealed identity type for the cross-context calling principal.
106pub const CALLER_ID: &str = "CallerId";
107
108/// A resolved actor contract: its scheme and the identity it yields.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct Contract {
111 pub scheme: Scheme,
112 pub identity: Identity,
113}
114
115/// The prelude actors — compiler-known boundary contracts available without a
116/// declaration. They back the per-protocol defaults and let public HTTP routes
117/// write `by v: Visitor` without ceremony.
118pub fn prelude_actor(name: &str) -> Option<Contract> {
119 Some(match name {
120 // Anonymous public surface — the only safe HTTP actor in Foundations.
121 "Visitor" => Contract {
122 scheme: Scheme::None,
123 identity: Identity::Unit,
124 },
125 // Platform schedulers / producers — Internal, carrying no useful
126 // identity payload (a bare tag).
127 "Scheduler" | "Producer" => Contract {
128 scheme: Scheme::Internal,
129 identity: Identity::Unit,
130 },
131 // The cross-context calling principal — Internal, yielding the sealed
132 // `CallerId` (Q7).
133 "Caller" => Contract {
134 scheme: Scheme::Internal,
135 identity: Identity::CallerId,
136 },
137 _ => return None,
138 })
139}
140
141/// The default actor a handler inherits when it omits `by`, by protocol (ADR
142/// Q5). HTTP has no safe default — `by` is required there.
143pub fn default_actor(protocol: &ServiceProtocol) -> Option<&'static str> {
144 match protocol {
145 ServiceProtocol::Call => Some("Caller"),
146 ServiceProtocol::Cron => Some("Scheduler"),
147 ServiceProtocol::Queue { .. } => Some("Producer"),
148 // Events track, slice 0 (spine #936): delivery is runtime-triggered,
149 // not an external caller — like `Queue`'s "Producer", the default
150 // actor names the event's originating publisher.
151 ServiceProtocol::Events { .. } => Some("Publisher"),
152 // v0.103: like HTTP, a WebSocket upgrade has no safe default actor —
153 // `by` is mandatory on `on open` (edge auth before accept, D-A).
154 ServiceProtocol::Http | ServiceProtocol::WebSocket { .. } => None,
155 }
156}
157
158/// v0.47: the data the emitter needs to lower a Bearer verification seam for a
159/// handler — the `by` binder (v0.50: `None` for the binder-less verify-and-
160/// discard form), the signing-secret env name, and the identity type to
161/// construct from the JWT `sub` claim. Resolved only for a handler whose `by`
162/// clause names a local Bearer actor; the checker guarantees the secret is
163/// present and the identity is a string-constructible local type.
164#[derive(Debug, Clone)]
165pub struct BearerSeam {
166 /// The identity binder, or `None` for `by <BearerActor>` (verify the token,
167 /// don't capture the identity). When `None` the seam still verifies fail-
168 /// closed but mints no identity and threads nothing into `deps`.
169 pub binder: Option<String>,
170 pub secret: String,
171 pub identity_type: String,
172 /// v0.53: the authorisation invariant when the `by` actor is a refinement
173 /// (`actor Admin = User where <pred>`). The seam verifies the scheme (401),
174 /// then checks this predicate against the verified claims (403 fail-closed),
175 /// then mints the (base) identity. `None` for a plain Bearer actor.
176 pub authorization: Option<ClaimPredicate>,
177}
178
179/// Resolve a handler's Bearer seam, if its `by` clause names a local Bearer
180/// actor — or a **refinement** of one (v0.53), following the refinement to its
181/// base for the scheme/secret/identity and carrying the authorisation
182/// predicate. Returns `None` for non-Bearer handlers (prelude actors are never
183/// Bearer) — those emit unchanged.
184/// #706: whether a `by` clause names a **Bearer**-secured actor (following a
185/// refinement to its base). The routes for which `by Nobody` can drive the auth
186/// seam to a `401` — an unsecured (`Visitor`/`None`) or `Signature`/`Oidc` route
187/// has no Bearer seam the test driver knows how to leave unauthenticated, so
188/// `by Nobody` there is rejected (`bynk.test.nobody_needs_secured_route`). The
189/// scheme check mirrors [`bearer_seam_for`].
190pub fn by_clause_is_bearer(by: &ByClause, actors: &HashMap<String, ActorDecl>) -> bool {
191 let Some(named) = actors.get(&by.primary().name) else {
192 return false;
193 };
194 let base = match &named.refinement {
195 Some(r) => match actors.get(&r.base.name) {
196 Some(b) => b,
197 None => return false,
198 },
199 None => named,
200 };
201 base.auth
202 .as_ref()
203 .and_then(|a| Scheme::from_name(a.name.as_str()))
204 == Some(Scheme::Bearer)
205}
206
207pub fn bearer_seam_for(
208 handler: &Handler,
209 actors: &HashMap<String, ActorDecl>,
210) -> Option<BearerSeam> {
211 let by = handler.by_clause.as_ref()?;
212 let named = actors.get(&by.primary().name)?;
213 // Follow a refinement to its base; carry the authorisation predicate. The
214 // checker guarantees a refinement's base is Bearer and its predicate parses.
215 let (base, authorization) = match &named.refinement {
216 Some(r) => (
217 actors.get(&r.base.name)?,
218 parse_claim_predicate(&r.predicate).ok(),
219 ),
220 None => (named, None),
221 };
222 if Scheme::from_name(base.auth.as_ref()?.name.as_str()) != Some(Scheme::Bearer) {
223 return None;
224 }
225 let secret = base.scheme_arg("secret")?.value.as_str()?.to_string();
226 let TypeRef::Named(id) = base.identity.as_ref()? else {
227 return None;
228 };
229 Some(BearerSeam {
230 binder: by.binder.as_ref().map(|b| b.name.clone()),
231 secret,
232 identity_type: id.name.clone(),
233 authorization,
234 })
235}
236
237/// v0.151: the data the emitter needs to lower an OIDC/JWKS verification seam —
238/// the `by` binder (or `None` for the verify-and-discard form), the public
239/// trust parameters (`issuer`, `audience`, `jwks` URL), and the identity type
240/// to construct from the verified `sub` claim. Resolved only for a handler
241/// whose `by` clause names a local `Oidc` actor. Unlike Bearer/Signature, the
242/// seam carries **no secret env name** — the trust root is the provider's
243/// published public key set (`jwks`).
244#[derive(Debug, Clone)]
245pub struct OidcSeam {
246 /// The identity binder, or `None` for `by <OidcActor>` (verify, don't
247 /// capture the identity). When `None` the seam still verifies fail-closed
248 /// but mints no identity and threads nothing into `deps`.
249 pub binder: Option<String>,
250 /// The expected `iss` claim (and the provider whose JWKS anchors trust).
251 pub issuer: String,
252 /// The expected `aud` claim (this API's audience identifier).
253 pub audience: String,
254 /// The JWKS endpoint URL the verifier fetches signing keys from.
255 pub jwks: String,
256 /// The context-owned, string-constructible identity type minted from `sub`.
257 pub identity_type: String,
258}
259
260/// Resolve a handler's OIDC seam, if its `by` clause names a single local
261/// `Oidc` actor. Returns `None` for non-Oidc handlers, for a multi-actor `by`
262/// clause (Oidc is single-actor this slice), and for a refinement (refinement
263/// over Oidc is a later slice) — those follow the existing seam paths. The
264/// checker guarantees `issuer`/`audience`/`jwks` are present and the identity
265/// is a string-constructible local type.
266pub fn oidc_seam_for(handler: &Handler, actors: &HashMap<String, ActorDecl>) -> Option<OidcSeam> {
267 let by = handler.by_clause.as_ref()?;
268 if by.is_sum() {
269 return None;
270 }
271 let actor = actors.get(&by.primary().name)?;
272 // A refinement's `auth` is `None`; it falls through here and follows the
273 // Bearer refinement path (or is rejected). An Oidc base is not narrowed.
274 if Scheme::from_name(actor.auth.as_ref()?.name.as_str()) != Some(Scheme::Oidc) {
275 return None;
276 }
277 let issuer = actor.scheme_arg("issuer")?.value.as_str()?.to_string();
278 let audience = actor.scheme_arg("audience")?.value.as_str()?.to_string();
279 let jwks = actor.scheme_arg("jwks")?.value.as_str()?.to_string();
280 let TypeRef::Named(id) = actor.identity.as_ref()? else {
281 return None;
282 };
283 Some(OidcSeam {
284 binder: by.binder.as_ref().map(|b| b.name.clone()),
285 issuer,
286 audience,
287 jwks,
288 identity_type: id.name.clone(),
289 })
290}
291
292/// v0.54: the binder of a cross-context `on call … by c: Caller` handler that
293/// captures a live `CallerId` (the calling context's name, Q7). `None` unless
294/// the handler binds an identity whose contract is `CallerId` — i.e. the
295/// `Caller` prelude actor (the only source of `CallerId`). A binder-less
296/// `on call` (or one inheriting the `Caller` default) captures nothing and is
297/// unaffected.
298pub fn caller_binder_for(handler: &Handler, actors: &HashMap<String, ActorDecl>) -> Option<String> {
299 // `CallerId` is a cross-context `on call` concept; the checker rejects a
300 // `Caller` actor on other protocols (`scheme_not_admissible`), but guard here
301 // too so the caller seam is never emitted off the call path.
302 if !matches!(handler.kind, HandlerKind::Call) {
303 return None;
304 }
305 let by = handler.by_clause.as_ref()?;
306 let binder = by.binder.as_ref()?;
307 let name = &by.primary().name;
308 // `CallerId` is yielded only by the `Caller` prelude actor; a local actor
309 // never declares it. A binder that collides with a param is suppressed
310 // upstream, mirroring the other seams.
311 let is_caller = !actors.contains_key(name)
312 && prelude_actor(name).map(|c| c.identity) == Some(Identity::CallerId)
313 && !handler.params.iter().any(|p| p.name.name == binder.name);
314 is_caller.then(|| binder.name.clone())
315}
316
317/// v0.51: the data the emitter needs to lower a Signature verification seam —
318/// the signing-secret env name, the signature header, and an optional
319/// timestamp header + tolerance window for replay defence. Resolved only for a
320/// handler whose `by` clause names a local Signature actor.
321#[derive(Debug, Clone)]
322pub struct SignatureSeam {
323 pub secret: String,
324 pub header: String,
325 pub timestamp_header: Option<String>,
326 pub tolerance_secs: Option<i64>,
327}
328
329/// Resolve a handler's Signature seam, if its `by` clause names a local
330/// Signature actor. The checker guarantees `secret` and `header` are present.
331pub fn signature_seam_for(
332 handler: &Handler,
333 actors: &HashMap<String, ActorDecl>,
334) -> Option<SignatureSeam> {
335 let by = handler.by_clause.as_ref()?;
336 let actor = actors.get(&by.primary().name)?;
337 if Scheme::from_name(actor.auth.as_ref()?.name.as_str()) != Some(Scheme::Signature) {
338 return None;
339 }
340 signature_seam_from_decl(actor)
341}
342
343/// The Signature seam data carried by an actor declaration (its keyed config).
344/// Shared by the single-actor `signature_seam_for` and the multi-actor
345/// `sum_members_for`.
346fn signature_seam_from_decl(actor: &ActorDecl) -> Option<SignatureSeam> {
347 Some(SignatureSeam {
348 secret: actor.scheme_arg("secret")?.value.as_str()?.to_string(),
349 header: actor.scheme_arg("header")?.value.as_str()?.to_string(),
350 timestamp_header: actor
351 .scheme_arg("timestamp")
352 .and_then(|a| a.value.as_str())
353 .map(str::to_string),
354 tolerance_secs: actor.scheme_arg("tolerance").and_then(|a| a.value.as_int()),
355 })
356}
357
358/// v0.52: one resolved member of a multi-actor sum — the seam the emitter tries
359/// at that position in the first-wins order. `actor_name` is the variant tag the
360/// body matches on.
361#[derive(Debug, Clone)]
362pub struct SumMember {
363 pub actor_name: String,
364 pub seam: SumMemberSeam,
365}
366
367/// The verification a sum member contributes. `None` (a catch-all such as
368/// `Visitor`) always resolves, so it terminates the order.
369#[derive(Debug, Clone)]
370pub enum SumMemberSeam {
371 None,
372 Bearer {
373 secret: String,
374 identity_type: String,
375 },
376 Signature(SignatureSeam),
377}
378
379impl SumMember {
380 /// Whether resolving this member needs the raw request body read.
381 pub fn needs_body(&self) -> bool {
382 matches!(self.seam, SumMemberSeam::Signature(_))
383 }
384 /// The member's identity type name, if it mints one (Bearer). `None`/
385 /// Signature members carry a unit identity.
386 pub fn identity_type(&self) -> Option<&str> {
387 match &self.seam {
388 SumMemberSeam::Bearer { identity_type, .. } => Some(identity_type),
389 _ => None,
390 }
391 }
392}
393
394/// v0.52: resolve a handler's `by` clause into ordered sum members, if it names
395/// more than one actor. `None` for a single-actor handler (those keep the
396/// existing seam paths). The checker has already validated peer/scheme/
397/// reachability rules; this lowers the verified members for emission.
398pub fn sum_members_for(
399 handler: &Handler,
400 actors: &HashMap<String, ActorDecl>,
401) -> Option<Vec<SumMember>> {
402 let by = handler.by_clause.as_ref()?;
403 if !by.is_sum() {
404 return None;
405 }
406 let mut members = Vec::new();
407 for actor_ref in &by.actors {
408 let seam = if let Some(decl) = actors.get(&actor_ref.name) {
409 match Scheme::from_name(decl.auth.as_ref()?.name.as_str())? {
410 Scheme::None => SumMemberSeam::None,
411 Scheme::Bearer => {
412 let secret = decl.scheme_arg("secret")?.value.as_str()?.to_string();
413 let TypeRef::Named(id) = decl.identity.as_ref()? else {
414 return None;
415 };
416 SumMemberSeam::Bearer {
417 secret,
418 identity_type: id.name.clone(),
419 }
420 }
421 Scheme::Signature => SumMemberSeam::Signature(signature_seam_from_decl(decl)?),
422 // v0.151: `Oidc` is single-actor only this slice — the checker
423 // rejects it as a sum member (`bynk.actor.oidc_not_in_sum`), so
424 // a well-formed program never reaches here with an Oidc peer.
425 // Return `None` (fail closed → no sum emission) defensively.
426 Scheme::Oidc => return None,
427 Scheme::Internal => return None,
428 }
429 } else {
430 // A prelude actor: only `Visitor` (scheme `None`) is an HTTP peer.
431 match prelude_actor(&actor_ref.name) {
432 Some(c) if c.scheme == Scheme::None => SumMemberSeam::None,
433 _ => return None,
434 }
435 };
436 members.push(SumMember {
437 actor_name: actor_ref.name.clone(),
438 seam,
439 });
440 }
441 Some(members)
442}
443
444/// Whether `scheme` is admissible on `protocol` (the admissible-scheme-per-
445/// protocol check). HTTP admits `None` (public routes) and `Bearer` (an
446/// `Authorization` header is an HTTP concept); the internal protocols
447/// (call/cron/queue) admit `Internal`. `Signature` is still reserved.
448pub fn scheme_admissible(protocol: &ServiceProtocol, scheme: Scheme) -> bool {
449 match protocol {
450 ServiceProtocol::Http => {
451 matches!(
452 scheme,
453 Scheme::None | Scheme::Bearer | Scheme::Signature | Scheme::Oidc
454 )
455 }
456 // v0.103 (D-B): a WebSocket upgrade authenticates via `None` (anonymous)
457 // or `Bearer` — but the token is read from the `Sec-WebSocket-Protocol`
458 // subprotocol, since a browser `WebSocket` cannot set an `Authorization`
459 // header. `Signature` is rejected at the WS boundary: HMAC-over-body has
460 // no body on a handshake.
461 ServiceProtocol::WebSocket { .. } => {
462 matches!(scheme, Scheme::None | Scheme::Bearer)
463 }
464 // Events track, slice 0 (spine #936): delivery is an internal,
465 // runtime-triggered invocation, like `Call`/`Cron`/`Queue` — no
466 // external network request, so no external auth scheme applies.
467 ServiceProtocol::Call
468 | ServiceProtocol::Cron
469 | ServiceProtocol::Queue { .. }
470 | ServiceProtocol::Events { .. } => {
471 matches!(scheme, Scheme::Internal)
472 }
473 }
474}
475
476/// v0.53: the closed claim-predicate vocabulary for a refinement actor's `where`
477/// clause (`actor Admin = User where hasClaim("admin")`). Claims are untyped
478/// JSON, so the predicate is a closed set — `hasClaim`/`claimEquals` composed
479/// with `&&`/`||`/`!` — checked against the *verified* JWT claims at the
480/// boundary. A general typed-claims expression surface is a later slice.
481#[derive(Debug, Clone)]
482pub enum ClaimPredicate {
483 /// `hasClaim("name")` — the claim is present and truthy.
484 HasClaim(String),
485 /// `claimEquals("name", "value")` — the claim string-equals `value`.
486 ClaimEquals(String, String),
487 And(Box<ClaimPredicate>, Box<ClaimPredicate>),
488 Or(Box<ClaimPredicate>, Box<ClaimPredicate>),
489 Not(Box<ClaimPredicate>),
490}
491
492fn claim_str_lit(e: &Expr) -> Option<String> {
493 match &e.kind {
494 ExprKind::StrLit(s) => Some(s.clone()),
495 _ => None,
496 }
497}
498
499/// Recognise the closed claim-predicate vocabulary in a refinement `where`
500/// expression. `Err(span)` points at the first sub-expression outside the set
501/// (for `bynk.actor.refinement_predicate_unsupported`).
502pub fn parse_claim_predicate(e: &Expr) -> Result<ClaimPredicate, Span> {
503 match &e.kind {
504 ExprKind::Paren(inner) => parse_claim_predicate(inner),
505 ExprKind::BinOp(BinOp::And, l, r) => Ok(ClaimPredicate::And(
506 Box::new(parse_claim_predicate(l)?),
507 Box::new(parse_claim_predicate(r)?),
508 )),
509 ExprKind::BinOp(BinOp::Or, l, r) => Ok(ClaimPredicate::Or(
510 Box::new(parse_claim_predicate(l)?),
511 Box::new(parse_claim_predicate(r)?),
512 )),
513 ExprKind::UnaryOp(UnaryOp::Not, inner) => {
514 Ok(ClaimPredicate::Not(Box::new(parse_claim_predicate(inner)?)))
515 }
516 ExprKind::Call {
517 name,
518 type_args,
519 args,
520 } if type_args.is_empty() => match (name.name.as_str(), args.as_slice()) {
521 ("hasClaim", [a]) => claim_str_lit(a).map(ClaimPredicate::HasClaim).ok_or(a.span),
522 ("claimEquals", [a, b]) => match (claim_str_lit(a), claim_str_lit(b)) {
523 (Some(n), Some(v)) => Ok(ClaimPredicate::ClaimEquals(n, v)),
524 (None, _) => Err(a.span),
525 (_, None) => Err(b.span),
526 },
527 _ => Err(name.span),
528 },
529 _ => Err(e.span),
530 }
531}
532
533/// Lower a claim predicate to a JavaScript boolean expression over `claims_var`
534/// (the verified claims object, `Record<string, unknown>`). Used by the emitter
535/// for the refinement seam's 403 check.
536pub fn claim_predicate_to_js(pred: &ClaimPredicate, claims_var: &str) -> String {
537 match pred {
538 ClaimPredicate::HasClaim(name) => {
539 format!("Boolean({claims_var}[\"{}\"])", js_str_escape(name))
540 }
541 ClaimPredicate::ClaimEquals(name, value) => format!(
542 "({claims_var}[\"{}\"] === \"{}\")",
543 js_str_escape(name),
544 js_str_escape(value)
545 ),
546 ClaimPredicate::And(l, r) => format!(
547 "({} && {})",
548 claim_predicate_to_js(l, claims_var),
549 claim_predicate_to_js(r, claims_var)
550 ),
551 ClaimPredicate::Or(l, r) => format!(
552 "({} || {})",
553 claim_predicate_to_js(l, claims_var),
554 claim_predicate_to_js(r, claims_var)
555 ),
556 ClaimPredicate::Not(inner) => {
557 format!("(!{})", claim_predicate_to_js(inner, claims_var))
558 }
559 }
560}
561
562fn js_str_escape(s: &str) -> String {
563 s.replace('\\', "\\\\").replace('"', "\\\"")
564}