Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ When `requestId` is `null`, the message is treated as standalone and won't be ma

The existing `String`/JSON methods remain unchanged. To inspect protobuf payloads, create an interceptor with a decoder supplied by your app:

There is no protobuf feature flag or `DariConfig` toggle. Using this interceptor overload and the protobuf capture methods opts the bridge into protobuf inspection. See the [full guide](https://easyhooon.github.io/dari/docs/protobuf) or [Korean guide](https://easyhooon.github.io/dari/ko/docs/protobuf).

```kotlin
import com.easyhooon.dari.interceptor.PayloadPart.REQUEST
import com.easyhooon.dari.interceptor.PayloadPart.RESPONSE
Expand Down
29 changes: 1 addition & 28 deletions documentation/content/docs/interceptor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,34 +50,7 @@ Use `Dari.createInterceptor()` in app code so release builds compile against `da

## Protocol Buffers (optional)

The string/JSON interface remains the default. For protobuf payloads, provide a decoder when creating the interceptor:

```kotlin
val protobufInterceptor = Dari.createInterceptor(
tag = "OrderBridge",
protobufDecoder = ProtobufPayloadDecoder { payload, context ->
when (context.handlerName to context.part) {
"createOrder" to PayloadPart.REQUEST ->
JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload))
"createOrder" to PayloadPart.RESPONSE ->
JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload))
else -> null
}
},
)

protobufInterceptor?.onWebToAppProtobufRequest(
handlerName = "createOrder",
requestId = requestId,
requestData = protobufBytes,
)
```

The decoder belongs to the consuming app, so Dari does not require a specific protobuf runtime. Returning `null` marks the decoder as unavailable for that handler. Decode failures are captured without crashing message inspection. Decoding runs synchronously on the interceptor caller's thread, so keep decoder work bounded.

Base64 is not required by Dari. Decode Base64 before calling the interceptor when using a string-only bridge, or pass bytes received from an `ArrayBuffer` bridge directly.

The REQUEST and RESPONSE tabs offer `Decoded` and `Raw` views. Dari retains at most the first 4 KB of each protobuf payload for the raw preview and displays it as Hex and Base64 while preserving the original byte size and truncation state. The displayed Base64 is generated for inspection and does not imply that the bridge used Base64 transport.
Protobuf inspection has no feature flag and does not replace the string/JSON API. See the [Protocol Buffers guide](./protobuf) for activation requirements, decoder setup, bridge transport options, and all request/response methods.

## Methods

Expand Down
29 changes: 1 addition & 28 deletions documentation/content/docs/ko/interceptor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,34 +50,7 @@ val interceptor = Dari.createInterceptor(tag = "PaymentBridge")

## Protocol Buffers (선택 사항)

기존 String/JSON 인터페이스는 그대로 유지됩니다. protobuf 페이로드를 검사하려면 인터셉터를 생성할 때 앱의 decoder를 전달합니다:

```kotlin
val protobufInterceptor = Dari.createInterceptor(
tag = "OrderBridge",
protobufDecoder = ProtobufPayloadDecoder { payload, context ->
when (context.handlerName to context.part) {
"createOrder" to PayloadPart.REQUEST ->
JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload))
"createOrder" to PayloadPart.RESPONSE ->
JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload))
else -> null
}
},
)

protobufInterceptor?.onWebToAppProtobufRequest(
handlerName = "createOrder",
requestId = requestId,
requestData = protobufBytes,
)
```

decoder는 사용하는 앱이 제공하므로 Dari는 특정 protobuf runtime에 의존하지 않습니다. 해당 handler를 지원하지 않으면 `null`을 반환할 수 있으며, 디코딩 실패가 메시지 캡처를 중단시키지 않습니다. 디코딩은 인터셉터 호출 스레드에서 동기 실행되므로 작업량을 제한해야 합니다.

Base64는 Dari의 필수 조건이 아닙니다. 문자열 전용 브릿지에서는 Base64를 먼저 디코딩하고, `ArrayBuffer` 브릿지에서는 전달받은 bytes를 바로 사용할 수 있습니다.

REQUEST와 RESPONSE 탭에서는 `Decoded`와 `Raw` 보기를 제공합니다. Dari는 raw preview를 위해 protobuf 페이로드의 앞부분을 최대 4KB까지만 보관하며, 원본 바이트 크기와 잘림 여부를 유지한 채 Hex와 Base64로 표시합니다. 이 Base64는 검사를 위해 Dari가 생성한 표시 형식이며, 실제 브릿지가 Base64로 운반했다는 의미는 아닙니다.
protobuf 검사에는 별도 feature flag가 없으며 기존 String/JSON API를 대체하지 않습니다. 활성화 조건, decoder 설정, 브릿지 전송 방식 및 모든 요청/응답 메서드는 [Protocol Buffers 가이드](./protobuf)를 참고하세요.

## 메서드

Expand Down
2 changes: 1 addition & 1 deletion documentation/content/docs/ko/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Dari",
"pages": ["index", "setup", "configuration", "interceptor", "modules"]
"pages": ["index", "setup", "configuration", "interceptor", "protobuf", "modules"]
}
148 changes: 148 additions & 0 deletions documentation/content/docs/ko/protobuf.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: Protocol Buffers
description: 선택적 protobuf 페이로드 검사 활성화 및 연동 방법
---

## 활성화 방법

Protocol Buffers 지원은 Dari 1.6.0 이상에서 사용할 수 있습니다. 별도 feature flag나 `DariConfig` 토글은 없습니다.

인터셉터를 생성하고 호출하는 시점에 선택적으로 활성화합니다:

1. `ProtobufPayloadDecoder`를 전달해 인터셉터를 생성합니다.
2. protobuf 요청 및 응답 메서드에 원본 `ByteArray`를 전달합니다.

`ProtobufDariInterceptor`는 `DariInterceptor`를 확장하므로 같은 인터셉터에서 기존 String/JSON 메서드도 계속 사용할 수 있습니다.

| 조건 | 필요 여부 | 설명 |
| --- | --- | --- |
| Dari 1.6.0 이상 | 필요 | `dari`와 `dari-noop` 버전을 동일하게 사용 |
| `protobufEnabled` 플래그 | 불필요 | protobuf 설정 플래그는 없음 |
| 앱의 protobuf runtime 및 generated message | 필요 | Dari는 runtime을 추가하거나 선택하지 않음 |
| 표시 문자열을 반환하는 decoder | 필요 | 알 수 없는 handler에서는 `null` 반환 가능 |
| Base64 | 문자열 전용 브릿지만 필요 | Dari에는 디코딩된 `ByteArray`를 전달 |
| `.proto` 또는 메시지 필드 변경 | 불필요 | 기존 schema와 wire payload를 그대로 사용 |

## 1. protobuf 지원 인터셉터 생성

schema를 이용한 디코딩은 앱이 담당합니다. handler와 요청/응답 타입에 맞는 사람이 읽을 수 있는 문자열을 반환합니다. 가능하면 JSON을 반환하면 Dari의 Decoded 화면에서 보기 좋게 정리됩니다.

```kotlin
import com.easyhooon.dari.Dari
import com.easyhooon.dari.interceptor.PayloadPart.REQUEST
import com.easyhooon.dari.interceptor.PayloadPart.RESPONSE
import com.easyhooon.dari.interceptor.ProtobufDariInterceptor
import com.easyhooon.dari.interceptor.ProtobufPayloadDecoder

val interceptor: ProtobufDariInterceptor? = Dari.createInterceptor(
tag = "OrderBridge",
protobufDecoder = ProtobufPayloadDecoder { payload, context ->
when (context.handlerName to context.part) {
"createOrder" to REQUEST ->
JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload))
"createOrder" to RESPONSE ->
JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload))
else -> null
}
},
)
```

Raw 데이터만 검사하려면 decoder가 의도적으로 `null`을 반환해도 됩니다:

```kotlin
val interceptor = Dari.createInterceptor(
protobufDecoder = ProtobufPayloadDecoder { _, _ -> null },
)
```

이 경우 Dari는 페이로드를 protobuf로 기록하고 decode 상태를 `DECODER_UNAVAILABLE`로 표시하며, raw preview와 원본 바이트 크기는 그대로 보존합니다.

## 2. 원본 바이트 캡처

기존 브릿지의 요청과 응답 처리 지점에서 Dari를 호출합니다. 캡처 전에 페이로드를 JSON으로 변환하지 않습니다.

```kotlin
interceptor?.onWebToAppProtobufRequest(
handlerName = "createOrder",
requestId = requestId,
requestData = requestBytes,
)

val response = CreateOrderResponse.newBuilder()
.setOrderId("order-123")
.build()

interceptor?.onWebToAppProtobufResponse(
handlerName = "createOrder",
requestId = requestId,
responseData = response.toByteArray(),
isSuccess = true,
)
```

요청과 응답을 연결하려면 동일하고 안정적인 `requestId`를 사용합니다. 단독 메시지 또는 fire-and-forget인 경우에만 `null`을 전달합니다.

## 방향별 메서드

| 브릿지 이벤트 | Dari 메서드 |
| --- | --- |
| Web이 App에 요청 | `onWebToAppProtobufRequest()` |
| App이 Web에 응답 | `onWebToAppProtobufResponse()` |
| App이 Web에 요청 | `onAppToWebProtobufRequest()` |
| Web이 App에 응답 | `onAppToWebProtobufResponse()` |

`handlerName`, 방향 및 요청/응답 구분은 `ProtobufDecodeContext`를 통해 decoder에 전달되므로 요청과 응답에 서로 다른 generated type을 사용할 수 있습니다.

## Base64와 브릿지 전송 방식

Base64는 protobuf나 Dari의 필수 조건이 아닙니다.

- 브릿지가 bytes를 제공하면 Dari에 바로 전달합니다.
- 문자열 전용 브릿지가 Base64를 운반하면 먼저 디코딩한 bytes를 전달합니다.

```kotlin
@JavascriptInterface
fun onProtobufRequest(requestId: String, base64Data: String) {
val bytes = Base64.decode(base64Data, Base64.NO_WRAP)
interceptor?.onWebToAppProtobufRequest(
handlerName = "createOrder",
requestId = requestId,
requestData = bytes,
)
}
```

Dari의 Raw 화면에 표시되는 Base64는 검사를 위해 생성된 표시 형식이며, 앱이 Base64로 페이로드를 운반했다는 의미가 아닙니다.

## Dari에 표시되는 내용

REQUEST와 RESPONSE 탭에서 두 가지 보기를 제공합니다:

- **Decoded**: 앱이 제공한 decoder가 반환한 문자열
- **Raw**: 캡처한 protobuf bytes를 Hex와 Base64로 표시

`Decoded`와 `Raw` 선택은 REQUEST와 RESPONSE 탭에서 각각 제공됩니다. 따라서 브릿지 요청과 응답 모두에서 schema로 해석한 값과 실제 wire bytes를 비교할 수 있습니다.

<table>
<thead>
<tr>
<th>Decoded</th>
<th>Raw (Hex 및 Base64)</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="/dari/protobuf-decoded.png" alt="Dari에서 디코딩된 protobuf 응답" width="360" /></td>
<td><img src="/dari/protobuf-raw.png" alt="Dari에서 Hex와 Base64로 표시된 protobuf 원본 응답" width="360" /></td>
</tr>
</tbody>
</table>

스크린샷은 RESPONSE 탭의 예시이며 REQUEST 탭에서도 동일한 두 가지 보기를 제공합니다. Dari는 원본 바이트 크기와 decode 상태를 기록합니다. Raw 캡처는 앞부분 최대 4KB로 제한되며, 더 큰 페이로드는 잘림 상태로 표시됩니다. 디코딩 실패는 브릿지 통신을 중단시키지 않고 기록됩니다.

## 런타임 동작

- 디코딩은 인터셉터 호출 스레드에서 동기 실행되므로 파싱과 표시 변환 작업을 제한해야 합니다.
- 릴리즈 빌드는 `dari-noop`을 사용하며 `Dari.createInterceptor()`가 `null`을 반환하므로 safe call에는 런타임 오버헤드가 없습니다.
- 동일한 protobuf 지원 인터셉터에서 기존 String/JSON 호출을 계속 사용할 수 있습니다.
2 changes: 1 addition & 1 deletion documentation/content/docs/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Dari",
"pages": ["index", "setup", "configuration", "interceptor", "modules"]
"pages": ["index", "setup", "configuration", "interceptor", "protobuf", "modules"]
}
148 changes: 148 additions & 0 deletions documentation/content/docs/protobuf.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: Protocol Buffers
description: Enable and integrate optional protobuf payload inspection
---

## Activation

Protocol Buffers support is available in Dari 1.6.0 and later. There is no feature flag and no `DariConfig` toggle for it.

The integration is opt-in at the interceptor boundary:

1. Create the interceptor with a `ProtobufPayloadDecoder`.
2. Pass the original `ByteArray` to the protobuf request and response methods.

The same interceptor still supports every existing String/JSON method because `ProtobufDariInterceptor` extends `DariInterceptor`.

| Requirement | Needed? | Notes |
| --- | --- | --- |
| Dari 1.6.0 or later | Yes | Use matching `dari` and `dari-noop` versions |
| `protobufEnabled` flag | No | No protobuf configuration flag exists |
| App-owned protobuf runtime and generated messages | Yes | Dari does not add or select a protobuf runtime |
| Decoder that returns display text | Yes | It may return `null` when a handler is unknown |
| Base64 | Only for string-only bridges | Dari itself accepts the decoded `ByteArray` |
| Changes to `.proto` files or message fields | No | Existing schemas and wire payloads remain unchanged |

## 1. Create a protobuf-capable interceptor

The application owns schema-aware decoding. Return a human-readable string for each handler and request/response type; JSON is recommended when convenient because Dari formats it in the decoded view.

```kotlin
import com.easyhooon.dari.Dari
import com.easyhooon.dari.interceptor.PayloadPart.REQUEST
import com.easyhooon.dari.interceptor.PayloadPart.RESPONSE
import com.easyhooon.dari.interceptor.ProtobufDariInterceptor
import com.easyhooon.dari.interceptor.ProtobufPayloadDecoder

val interceptor: ProtobufDariInterceptor? = Dari.createInterceptor(
tag = "OrderBridge",
protobufDecoder = ProtobufPayloadDecoder { payload, context ->
when (context.handlerName to context.part) {
"createOrder" to REQUEST ->
JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload))
"createOrder" to RESPONSE ->
JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload))
else -> null
}
},
)
```

If only raw inspection is needed, the decoder can intentionally return `null`:

```kotlin
val interceptor = Dari.createInterceptor(
protobufDecoder = ProtobufPayloadDecoder { _, _ -> null },
)
```

Dari will record the payload as protobuf with `DECODER_UNAVAILABLE` while keeping its raw preview and original byte size.

## 2. Capture the original bytes

Call Dari at the same request and response boundaries already used by the bridge. Do not convert the payload to JSON before capture.

```kotlin
interceptor?.onWebToAppProtobufRequest(
handlerName = "createOrder",
requestId = requestId,
requestData = requestBytes,
)

val response = CreateOrderResponse.newBuilder()
.setOrderId("order-123")
.build()

interceptor?.onWebToAppProtobufResponse(
handlerName = "createOrder",
requestId = requestId,
responseData = response.toByteArray(),
isSuccess = true,
)
```

Use a stable `requestId` to pair a request and response. Pass `null` only for standalone or fire-and-forget messages.

## Direction and method mapping

| Bridge event | Dari method |
| --- | --- |
| Web sends request to App | `onWebToAppProtobufRequest()` |
| App responds to Web | `onWebToAppProtobufResponse()` |
| App sends request to Web | `onAppToWebProtobufRequest()` |
| Web responds to App | `onAppToWebProtobufResponse()` |

`handlerName`, direction, and request/response part are provided to the decoder through `ProtobufDecodeContext`, allowing request and response messages to use different generated types.

## Base64 and bridge transport

Base64 is not required by protobuf or Dari.

- If the bridge already provides bytes, pass them directly to Dari.
- If a string-only bridge transports Base64, decode it first and pass the resulting bytes.

```kotlin
@JavascriptInterface
fun onProtobufRequest(requestId: String, base64Data: String) {
val bytes = Base64.decode(base64Data, Base64.NO_WRAP)
interceptor?.onWebToAppProtobufRequest(
handlerName = "createOrder",
requestId = requestId,
requestData = bytes,
)
}
```

The Base64 shown in Dari's Raw view is generated for display and does not indicate how the application transported the payload.

## What Dari displays

The REQUEST and RESPONSE tabs provide two views:

- **Decoded**: text returned by the application-provided decoder
- **Raw**: the captured protobuf bytes rendered as Hex and Base64

The `Decoded` and `Raw` controls are available independently in both the REQUEST and RESPONSE tabs. This lets you compare the schema-aware value with the actual wire bytes for each side of the bridge exchange.

<table>
<thead>
<tr>
<th>Decoded</th>
<th>Raw (Hex and Base64)</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="/dari/protobuf-decoded.png" alt="Decoded protobuf response in Dari" width="360" /></td>
<td><img src="/dari/protobuf-raw.png" alt="Raw protobuf response as Hex and Base64 in Dari" width="360" /></td>
</tr>
</tbody>
</table>

The screenshots show the RESPONSE tab; the REQUEST tab provides the same two views. Dari records the original byte size and decode status. The raw capture is limited to the first 4 KB, and larger payloads are marked as truncated. Decoder failures are recorded without interrupting bridge communication.

## Runtime behavior

- Decoding runs synchronously on the interceptor caller's thread, so keep parsing and display conversion bounded.
- Release builds use `dari-noop`; `Dari.createInterceptor()` returns `null`, and safe calls have no runtime overhead.
- Existing String/JSON calls can continue on the same protobuf-capable interceptor.
Binary file added documentation/public/protobuf-decoded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added documentation/public/protobuf-raw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading