1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! gRPC envelope-signing canonicalization for the native Rust client —
//! mirrors `cratestack_grpc::canonical::grpc_canonical_request_string`
//! (`docs/design/protobuf.md` §7.3), adapted for the client side of the
//! wire. This is the ticket's highest-risk decision (see #209's own risk
//! note); the reasoning below is the answer, not an assumption.
//!
//! **Why this does not reuse `cratestack_grpc::canonical` directly, and
//! why that's still correct:**
//!
//! `cratestack_grpc::canonical::grpc_canonical_request_string` canonicalizes
//! *received* wire bytes: it accepts the already length-prefix-*framed*
//! body exactly as `tonic::server::Grpc::unary` hands it to a service impl
//! (see that crate's `framing.rs` module doc), and strips the 5-byte frame
//! header before hashing — because on the server side, framed bytes are
//! all that's available to intercept (`tonic`'s own `Codec`/`Decoder`
//! chain decodes the message before any CrateStack code sees it; there is
//! no lower-level hook to grab raw bytes from — see that module's "Known
//! gap" note).
//!
//! On the **client** side there is no equivalent framed-bytes interception
//! point to begin with, and none is needed: this crate builds the request
//! message, canonicalizes it, *then* hands it to
//! `tonic::client::Grpc::unary`, which does the framing internally on the
//! way out. Signing happens strictly before framing exists. So the
//! correct client-side input is `prost::Message::encode_to_vec()`'s
//! output directly — which is *already* the same unframed bytes
//! `strip_grpc_frame` would produce if it ran on this crate's output after
//! tonic re-framed it. In other words: both sides converge on hashing the
//! identical byte sequence (the prost-encoded message, no frame), just
//! reached by different means — the server strips a frame down to it, the
//! client never adds one in the first place. `grpc_canonical_request_string`'s
//! own test (`canonical_string_matches_direct_core_call_on_unframed_bytes`)
//! already proves the server's stripped result equals calling
//! `cratestack_core::canonical_request_string` on the raw message bytes
//! directly — this module's `grpc_canonical_request_string` **is** that
//! direct call, so the two are provably byte-identical for the same
//! (package, method, message) triple without needing to share code.
//!
//! This also means the ticket's flagged risk — "don't assume this is a
//! drop-in reuse of the TS gRPC-Web client's base64/trailer-frame-specific
//! logic" — does not apply here at all: the TS client's constraints
//! (`grpc-web-text+proto`'s base64 encoding, gRPC-Web's response trailer
//! frame) are wire-transport peculiarities of gRPC-Web specifically, and a
//! native `tonic` client speaks plain binary gRPC framing, which (per
//! `cratestack_grpc::framing`'s own module doc) is exactly what a
//! `tonic::server::Grpc`-based CrateStack server always sees regardless of
//! which client dialect originated the call. Nothing gRPC-Web-specific
//! ever enters this module.
//!
//! **Why this is a small reimplementation of two constants rather than a
//! dependency on `cratestack-grpc`:** that crate is the *server* runtime —
//! it unconditionally depends on `axum`/`tonic-web`/`tower-http` (CORS,
//! gRPC-Web translation), none of which a pure gRPC client binary needs.
//! Pulling it in just to reach `GRPC_CONTENT_TYPE` and `grpc_method_path`
//! (two lines each) would tax every gRPC client build for zero benefit —
//! the same "small pure mapping gets reimplemented per crate/module"
//! precedent already established multiple times in this codebase (see
//! `cratestack-client-typescript::grpc::wire`'s module doc and
//! `cratestack-macros::include::client::grpc::rpc_inputs`'s module doc).
//! The cross-crate test below pins the two copies together so a future
//! change to either format doesn't drift silently.
use canonical_request_string;
/// Pinned negotiated content type for every CrateStack gRPC call — same
/// value `cratestack_grpc::canonical::GRPC_CONTENT_TYPE` uses server-side
/// (`docs/design/protobuf.md` §7.3: no content negotiation on this
/// binding).
pub const GRPC_CONTENT_TYPE: &str = "application/grpc+proto";
/// `/<package>.Api/<MethodName>` — same shape as
/// `cratestack_grpc::canonical::grpc_method_path`.
/// Builds the canonical signing string for an outgoing gRPC call from the
/// **unframed** prost-encoded request bytes. `query` is always `None` —
/// gRPC has no query string — matching
/// `cratestack_grpc::canonical::grpc_canonical_request_string`'s output
/// exactly for the same (package, method, message) triple. See the module
/// doc for why no frame-stripping step belongs here.