Skip to content

Commit e8109af

Browse files
committed
docs(by-design): add indonesian translation
- Add Bearer Auth page translated to Indonesian - Add Caching page translated to Indonesian - Add Compression page translated to Indonesian - Add Distributed Tracing page translated to Indonesian - Add HTTPS Redirect page translated to Indonesian - Add Locale Redirect page translated to Indonesian - Add Method Override page translated to Indonesian - Add Pretty JSON page translated to Indonesian - Add Rate Limiting page translated to Indonesian - Add Request ID page translated to Indonesian - Add Server-Timing page translated to Indonesian - Add XSS Input Sanitizer page translated to Indonesian - Add overview index translated to Indonesian
1 parent fb38156 commit e8109af

13 files changed

Lines changed: 975 additions & 0 deletions

‎docs/id/by-design/bearer-auth.md‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
description: "Kenapa Deserve membawa Basic Auth tapi tidak Bearer, dan cara menyusun auth token untuk skema apa pun."
3+
---
4+
5+
# Bearer Auth
6+
7+
Deserve membawa [Basic Auth](/id/middleware/basic-auth) tapi tidak ada middleware Bearer, dan pemisahan antara keduanya adalah intinya.
8+
9+
## Kenapa Tidak Dibawa
10+
11+
Bearer hanyalah sebuah amplop. Header [`Authorization: Bearer <token>`](https://datatracker.ietf.org/doc/html/rfc6750) membawa token, dan apa yang dianggap token valid berubah seiring ekosistem. Satu layanan memverifikasi tanda tangan [JWT](https://datatracker.ietf.org/doc/html/rfc7519), lain mengambil kunci publik berputar dari endpoint [JWKS](https://datatracker.ietf.org/doc/html/rfc7517), lain memanggil API introspeksi untuk token buram, dan algoritma penanda bisa RS256, ES256, atau HS256.
12+
13+
Menanam salah satu pilihan itu akan mengunci setiap proyek ke satu skema. Ketika spesifikasi bergerak atau sebuah tim memutar kunci dengan cara berbeda, jawaban bawaan itu berubah jadi kandang ketimbang bantuan. Jadi keputusannya adalah membiarkan verifikasi terbuka dan membiarkan developer membawa skema yang dibutuhkan kasusnya.
14+
15+
## Kenapa Basic Auth Dibawa Tapi Bearer Tidak
16+
17+
[Basic Auth](/id/middleware/basic-auth) adalah satu skema tetap. Header membawa username dan password base64, pengecekannya adalah perbandingan waktu konstan terhadap sebuah daftar, dan tak ada yang perlu dipilih. Stabilitas itulah yang membuatnya cocok di dalam framework.
18+
19+
Bearer sebaliknya. Format token, tanda tangan, dan sumber kepercayaan semuanya berbeda, jadi tak ada satu pengecekan untuk dibawa. Kedua skema membaca header `Authorization` yang sama, tapi hanya satu yang punya jawaban benar tunggal.
20+
21+
## Bagian yang Sudah Ada
22+
23+
Penjaga token adalah komposisi kecil di atas bagian yang sudah ada:
24+
25+
- **Baca header** - [`ctx.header('authorization')`](/id/core-concepts/context-object#akses-data-request) mengembalikan nilai `Authorization` mentah.
26+
- **Jalan lebih awal** - [middleware global](/id/middleware/global) berjalan sebelum route handler dan bisa menghentikan request dengan mengembalikan `Response`.
27+
- **Tolak bersih** - [`ctx.handleError(401, ...)`](/id/core-concepts/context-object#penanganan-error) mengarah lewat [`router.catch()`](/id/error-handling/object-details) saat satu diatur.
28+
- **Bawa hasilnya** - [`ctx.state`](/id/core-concepts/context-object#berbagi-state) menyerahkan identitas terdekode ke handler di hilir.
29+
30+
## Sebuah Penjaga Bearer
31+
32+
Middleware ini menarik token dari header, memverifikasinya, dan menyimpan hasilnya untuk handler berikutnya. Placeholder `verifyToken` mewakili skema pilihan, sebuah pengecekan JWT, lookup JWKS, atau panggilan introspeksi.
33+
34+
```typescript twoslash
35+
import type { Context } from '@neabyte/deserve'
36+
import { Router } from '@neabyte/deserve'
37+
38+
const router = new Router()
39+
declare function verifyToken(token: string): Promise<{ userId: string } | null>
40+
// ---cut---
41+
router.use(async (ctx, next) => {
42+
const header = ctx.header('authorization')
43+
const spaceIndex = header ? header.indexOf(' ') : -1
44+
const scheme = spaceIndex > 0 ? header!.slice(0, spaceIndex) : ''
45+
46+
// Tolak apa pun yang bukan Bearer
47+
if (scheme.toLowerCase() !== 'bearer') {
48+
return await ctx.handleError(401, new Error('Missing Bearer token'))
49+
}
50+
51+
// Verifikasi dengan skema pilihan
52+
const token = header!.slice(spaceIndex + 1).trim()
53+
const claims = await verifyToken(token)
54+
if (!claims) {
55+
return await ctx.handleError(401, new Error('Invalid token'))
56+
}
57+
58+
// Serahkan identitas ke handler
59+
ctx.state.userId = claims.userId
60+
return await next()
61+
})
62+
63+
await router.serve(8000)
64+
```
65+
66+
Handler lalu membaca identitas langsung dari state, tanpa parsing token sendiri.
67+
68+
```typescript twoslash
69+
import type { Context } from '@neabyte/deserve'
70+
// ---cut---
71+
export function GET(ctx: Context): Response {
72+
// Baca apa yang disimpan penjaga
73+
const userId = ctx.state.userId
74+
return ctx.send.json({ userId })
75+
}
76+
```
77+
78+
## Mengarahkan Kegagalan Lewat Satu Handler
79+
80+
Penjaga di atas mengembalikan `401` dari dalam middleware. Untuk mengirim setiap kegagalan auth lewat satu tempat, bungkus middleware dengan [`WrapMware`](/id/middleware/global#membungkus-middleware-dengan-penanganan-error) dan lempar saat ditolak, lalu bentuk balasannya dengan [`router.catch()`](/id/error-handling/object-details).
81+
82+
```typescript twoslash
83+
import type { Context } from '@neabyte/deserve'
84+
import { Router, WrapMware } from '@neabyte/deserve'
85+
86+
const router = new Router()
87+
declare function verifyToken(token: string): Promise<{ userId: string } | null>
88+
// ---cut---
89+
// Lemparan sampai router.catch saat dibungkus
90+
const bearer = WrapMware('Bearer', async (ctx: Context, next) => {
91+
const header = ctx.header('authorization')
92+
if (!header?.toLowerCase().startsWith('bearer ')) {
93+
throw new Error('Missing Bearer token')
94+
}
95+
const claims = await verifyToken(header.slice(7).trim())
96+
if (!claims) {
97+
throw new Error('Invalid token')
98+
}
99+
ctx.state.userId = claims.userId
100+
return await next()
101+
})
102+
103+
// Terapkan penjaga dan bentuk error
104+
router.use(bearer)
105+
router.catch((ctx, err) => ctx.send.json({ error: err.error?.message }, { status: 401 }))
106+
```
107+
108+
Ini pola pembungkusan yang sama dipakai [Basic Auth](/id/middleware/basic-auth) secara internal, kini membawa pengecekan token alih-alih perbandingan kredensial.
109+
110+
## Menjaga Hanya Sebagian Rute
111+
112+
Penjaga token sering cocok di prefix API sementara halaman publik tetap terbuka. Middleware per-path membatasi pengecekan ke satu prefix, bentuk yang sama ditunjukkan di [middleware global](/id/middleware/global#middleware-per-path).
113+
114+
```typescript twoslash
115+
import type { Context } from '@neabyte/deserve'
116+
import { Router } from '@neabyte/deserve'
117+
118+
const router = new Router()
119+
declare function verifyToken(token: string): Promise<{ userId: string } | null>
120+
// ---cut---
121+
// Jaga hanya rute /api
122+
router.use('/api', async (ctx, next) => {
123+
const header = ctx.header('authorization')
124+
const claims = header?.toLowerCase().startsWith('bearer ')
125+
? await verifyToken(header.slice(7).trim())
126+
: null
127+
if (!claims) {
128+
return await ctx.handleError(401, new Error('Invalid token'))
129+
}
130+
ctx.state.userId = claims.userId
131+
return await next()
132+
})
133+
```
134+
135+
Untuk session sisi server yang ditandatangani framework alih-alih token, lihat [middleware session](/id/middleware/session).

‎docs/id/by-design/cache.md‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
description: "Kenapa Deserve tidak punya middleware cache, dan cara session stateless ditambah map memori polos memenuhi kebutuhan."
3+
---
4+
5+
# Caching
6+
7+
Deserve tidak membawa middleware cache, dan pipeline request tak menyimpan store tersembunyi sendiri. Itu menjaga perilaku memori tetap dapat ditebak, dan menyerahkan strategi caching ke developer.
8+
9+
## Kenapa Tidak Dibawa
10+
11+
Kebanyakan yang orang cari dari cache adalah menyimpan sedikit state per-pengguna lintas request. Tugas itu sudah ditangani. [Middleware session](/id/middleware/session) menyimpan data per-pengguna di cookie bertanda tangan, tanpa store sisi server yang tumbuh, kedaluwarsa, atau ter-evict. Untuk 80 persen yang umum, status login dan preferensi, datanya menumpang di klien dan server tetap stateless.
12+
13+
Sebuah middleware cache umum harus memilih kebijakan eviksi, strategi kunci, dan anggaran memori untuk semua orang sekaligus, dan pilihan itu milik aplikasi, bukan framework. Jadi keputusannya adalah meninggalkan store dan membiarkan developer menyimpan persis yang dibutuhkan kasusnya.
14+
15+
## Memori Tetap Bersih di Bawah Beban
16+
17+
Sebuah cache bawaan terasa menggoda karena memori bisa merayap naik seiring waktu. Kekhawatiran itu tak berlaku pada framework itu sendiri. Pipeline request mengalokasi per request dan membiarkan tiap [Context](/id/core-concepts/context-object) keluar dari scope begitu response dikirim, jadi tak ada tabel internal yang tumbuh dengan trafik. Di bawah trafik beban-tinggi berkelanjutan framework tidak membocorkan apa pun lintas pipeline, dan runtime merebut kembali tiap request yang selesai sendiri.
18+
19+
Garbage collection adalah tugas runtime, sejalan dengan [bangun di atas platform](/id/core-concepts/philosophy#bangun-di-atas-platform). Deno dan V8 memiliki heap dan kolektornya, dan Deserve tidak melawan maupun membungkusnya. Apa pun yang di-cache aplikasi tinggal di memori aplikasi, di mana umurnya adalah hak developer untuk memutuskan.
20+
21+
## Menyimpan Data di Memori
22+
23+
Ketika sebuah nilai memang perlu tinggal di server, sebuah `Map` polos yang dideklarasi di scope modul adalah seluruh polanya. Ia dibuat sekali dan dibagi ke setiap request ke modul itu.
24+
25+
```typescript twoslash
26+
import type { Context } from '@neabyte/deserve'
27+
// ---cut---
28+
// Satu store dibagi lintas request
29+
const cache = new Map<string, unknown>()
30+
31+
export async function GET(ctx: Context): Promise<Response> {
32+
const key = ctx.pathname
33+
34+
// Sajikan nilai cache ketika ada
35+
const hit = cache.get(key)
36+
if (hit !== undefined) {
37+
return ctx.send.json({ source: 'cache', data: hit })
38+
}
39+
40+
// Bangun sekali, lalu simpan untuk lain kali
41+
const data = await buildExpensiveData()
42+
cache.set(key, data)
43+
return ctx.send.json({ source: 'fresh', data })
44+
}
45+
46+
declare function buildExpensiveData(): Promise<unknown>
47+
```
48+
49+
## Membuat Entri Kedaluwarsa
50+
51+
Sebuah `Map` tak pernah meng-evict sendiri, jadi cache yang berjalan lama butuh time-to-live. Simpan nilai dengan cap kedaluwarsa dan perlakukan entri lama sebagai miss.
52+
53+
```typescript twoslash
54+
import type { Context } from '@neabyte/deserve'
55+
// ---cut---
56+
// Atur berapa lama entri tetap segar
57+
const ttlMs = 30_000
58+
const cache = new Map<string, { value: unknown, expiresAt: number }>()
59+
60+
export function GET(ctx: Context): Response {
61+
const key = ctx.pathname
62+
const entry = cache.get(key)
63+
64+
// Entri segar menang, yang lama dibuang
65+
if (entry && Date.now() < entry.expiresAt) {
66+
return ctx.send.json({ source: 'cache', data: entry.value })
67+
}
68+
69+
// Hitung ulang dan cap kedaluwarsa baru
70+
const value = { time: Date.now() }
71+
cache.set(key, { value, expiresAt: Date.now() + ttlMs })
72+
return ctx.send.json({ source: 'fresh', data: value })
73+
}
74+
```
75+
76+
Sebuah `Map` yang dikunci dengan data request tumbuh dengan tiap kunci berbeda, jadi batasi atau sapu entri kedaluwarsa di timer. Ketika kuncinya objek alih-alih string, sebuah `WeakMap` membiarkan runtime membuang entri begitu kuncinya hilang, yang cocok untuk metadata per-objek tanpa pembersihan manual.
77+
78+
## Ketika Memori Tidak Cukup
79+
80+
Dua kasus menuntut lebih dari map lokal-proses. Sebuah cache yang harus bertahan dari restart, atau dibagi lintas beberapa instance, cocok di store eksternal seperti [Redis](https://redis.io/), dicapai dengan klien biasa di dalam handler. Sebuah cache yang sekadar butuh ruang lebih bisa berjalan di memori setelah heap V8 dinaikkan saat startup. Ukuran heap adalah flag runtime, bukan setelan framework, jadi [dokumentasi Deno](https://docs.deno.com/runtime/) adalah tempat untuk invokasi persisnya.
81+
82+
## Berbagi Per-Request
83+
84+
Caching lintas request adalah satu kebutuhan, mengoper sebuah nilai sepanjang satu request adalah kebutuhan lain. Sebuah nilai yang dihitung di middleware dan dibaca handler tak masuk cache sama sekali, ia masuk [`ctx.state`](/id/core-concepts/context-object#berbagi-state), yang hidup persis satu request dan hilang saat response dikirim.

‎docs/id/by-design/compress.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
description: "Kenapa Deserve tidak punya middleware kompresi, karena Deno mengompresi response dan proxy menangani sisanya."
3+
---
4+
5+
# Kompresi
6+
7+
Deserve tidak membawa middleware kompresi, dan celah itu sudah terisi dua kali sebelum sebuah request menyentuh kode aplikasi.
8+
9+
## Kenapa Tidak Dibawa
10+
11+
Kompresi adalah urusan lapisan di sekitar handler, bukan di dalamnya. Runtime melakukannya saat keluar, dan di produksi sebuah proxy biasanya sudah melakukannya lebih dulu. Sebuah middleware yang mengulang pekerjaan yang sama hanya menambah biaya dan peluang bentrok dengan encoding yang sudah diatur lapisan itu.
12+
13+
Dua lapisan sudah menanganinya:
14+
15+
- **Runtime** - [`Deno.serve`](/id/getting-started/server-configuration) mengompresi body response sendiri, sejalan dengan [bangun di atas platform](/id/core-concepts/philosophy#bangun-di-atas-platform).
16+
- **Network edge** - kebanyakan aplikasi produksi berada di belakang proxy seperti [Cloudflare](https://developers.cloudflare.com/speed/optimization/content/compression/) atau [nginx](https://docs.nginx.com/nginx/admin-guide/web-server/compression/), di mana kompresi adalah tugas lapisan jaringan, bukan aplikasi.
17+
18+
Jadi middleware kompresi baru berguna hanya di localhost telanjang tanpa proxy di depan, dan bahkan di sana runtime sudah menanganinya.
19+
20+
## Yang Deno Lakukan Sendiri
21+
22+
`Deno.serve` membaca header `Accept-Encoding` dari request dan mengompresi body response ketika hal ini berlaku:
23+
24+
- Header mengiklankan `br` untuk Brotli atau `gzip`, dan preferensi nilai kualitas dihormati.
25+
- `Content-Type` response termasuk salah satu tipe yang bisa dikompresi.
26+
- Body response lebih besar dari 64 byte.
27+
28+
Ketika mengompresi, runtime mengatur `Content-Encoding` ke skema yang dipilih dan menyesuaikan header `Vary` supaya cache memakai encoding sebagai kunci. Tidak ada di handler yang perlu meminta ini.
29+
30+
Sebuah response dibiarkan tanpa kompresi ketika salah satu dari ini hadir, yang merupakan runtime sengaja menyingkir:
31+
32+
- Header `Content-Encoding`, berarti body sudah ter-encode.
33+
- Header `Content-Range`, berarti ada request rentang.
34+
- Header `Cache-Control: no-transform`, berarti tak ada lapisan yang boleh menulis ulang body.
35+
36+
## Membiarkan Runtime Mengompresi
37+
38+
Jalur default adalah mengirim response biasa dan membiarkan encoding apa adanya. Body JSON atau teks polos mengalir lewat, dan runtime mengompresinya ketika klien mendukung.
39+
40+
```typescript twoslash
41+
import type { Context } from '@neabyte/deserve'
42+
// ---cut---
43+
export function GET(ctx: Context): Response {
44+
// Kirim polos, runtime kompresi bila bisa
45+
return ctx.send.json({ message: 'compressed by the runtime' })
46+
}
47+
```
48+
49+
## Mengeluarkan Satu Response
50+
51+
Untuk menjaga satu response tetap tanpa kompresi, atur header yang diperlakukan runtime sebagai sinyal berhenti. Sebuah `Cache-Control: no-transform` memberi tahu runtime dan proxy hilir untuk tidak menyentuh body.
52+
53+
```typescript twoslash
54+
import type { Context } from '@neabyte/deserve'
55+
// ---cut---
56+
export function GET(ctx: Context): Response {
57+
// Larang lapisan apa pun menulis ulang body
58+
ctx.setHeader('Cache-Control', 'no-transform')
59+
return ctx.send.json({ message: 'sent verbatim' })
60+
}
61+
```
62+
63+
Mengatur header lewat [`ctx.setHeader`](/id/core-concepts/context-object#header-response) adalah jalur yang sama dipakai di tempat lain, jadi opt-out ini terbaca seperti header lain.
64+
65+
## Body yang Sudah Ter-encode
66+
67+
Body yang datang sudah terkompresi, seperti aset `.gz` tersimpan, membawa `Content-Encoding`-nya sendiri. Runtime melihat header itu dan melewati kompresi ulang, jadi byte dikirim apa adanya. Untuk file di disk ini ditangani oleh [penyajian statis](/id/static-file/basic), yang mengatur tipe dan membiarkan runtime memutuskan.
68+
69+
## Di Belakang Proxy
70+
71+
Di deployment yang difrontkan Cloudflare atau nginx, proxy menegosiasikan kompresi dengan klien di edge, sering sebelum request mencapai origin. Origin tetap bisa mengirim response biasa, dan lapisan-lapisan sepakat soal encoding lewat header `Accept-Encoding` dan `Vary`. Aplikasi tetap di luar urusan ini, yang memang intinya.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
description: "Kenapa Deserve tidak punya middleware HTTPS redirect, karena TLS ada di edge dan redirect paksa di aplikasi bisa berputar."
3+
---
4+
5+
# HTTPS Redirect
6+
7+
Deserve tidak punya middleware HTTPS redirect, dan memaksakannya di dalam aplikasi adalah lapisan yang salah untuk pekerjaan itu. TLS diterminasi di edge, jadi aplikasi jarang melihat skema asli sejak awal.
8+
9+
## Kenapa Tidak Dibawa
10+
11+
Sebuah HTTPS redirect paksa mengirim request `http://` apa pun kembali sebagai `https://`. Masalahnya ada di mana TLS sebenarnya berada. Di produksi sertifikat duduk di proxy atau load balancer seperti [Cloudflare](https://www.cloudflare.com/learning/ssl/what-is-ssl/) atau [nginx](https://nginx.org/en/docs/http/configuring_https_servers.html), yang menterminasi TLS dan meneruskan HTTP polos ke origin. Aplikasi lalu melihat `http`, meski klien terhubung lewat `https`, jadi redirect yang dibangun dari skema lokal akan memantulkan request yang sudah aman.
12+
13+
Begitulah redirect loop terjadi. Proxy bicara HTTPS ke klien, origin melihat HTTP dan redirect ke HTTPS, proxy meneruskan HTTP lagi, dan loop tak pernah berakhir. Tempat aman untuk menegakkan HTTPS adalah lapisan yang memiliki sertifikat, sejalan dengan [bangun di atas platform](/id/core-concepts/philosophy#bangun-di-atas-platform).
14+
15+
## Di Mana HTTPS Berada
16+
17+
Redirect-nya sendiri adalah satu baris konfigurasi proxy, bukan kode aplikasi:
18+
19+
- **Di proxy** - [Cloudflare](https://developers.cloudflare.com/ssl/edge-certificates/additional-options/always-use-https/) dan [nginx](https://nginx.org/en/docs/http/configuring_https_servers.html) redirect `http` ke `https` sebelum trafik mencapai origin, jadi aplikasi hanya pernah menyajikan request yang aman.
20+
- **Di browser** - header `Strict-Transport-Security` memberi tahu browser untuk memakai HTTPS sendiri pada kunjungan berikutnya, yang menghapus redirect setelah response aman pertama.
21+
22+
Deserve sudah menangani separuh kedua. [Middleware security headers](/id/middleware/security-headers#stricttransportsecurity) mengatur HSTS lewat opsi `strictTransportSecurity`, mati secara default dan dimaksudkan menyala setelah server dicapai lewat HTTPS.
23+
24+
```typescript twoslash
25+
import { Mware, Router } from '@neabyte/deserve'
26+
27+
const router = new Router()
28+
// ---cut---
29+
// Suruh browser tetap pakai HTTPS
30+
router.use(
31+
Mware.securityHeaders({
32+
strictTransportSecurity: 'max-age=31536000; includeSubDomains'
33+
})
34+
)
35+
36+
await router.serve(8000)
37+
```
38+
39+
## Membaca Skema Asli
40+
41+
Ketika aplikasi memang perlu tahu apakah klien memakai HTTPS, jawabannya ada di header forwarded yang diatur proxy, bukan di koneksi lokal. Proxy tepercaya menambahkan [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto), dibaca lewat [`ctx.header`](/id/core-concepts/context-object#akses-data-request).
42+
43+
```typescript twoslash
44+
import type { Context } from '@neabyte/deserve'
45+
// ---cut---
46+
export function GET(ctx: Context): Response {
47+
// Skema yang benar-benar dipakai klien
48+
const proto = ctx.header('x-forwarded-proto') ?? 'http'
49+
return ctx.send.json({ secure: proto === 'https' })
50+
}
51+
```
52+
53+
Percayai header ini hanya di belakang proxy yang dikonfigurasi lewat [`trustProxy`](/id/getting-started/server-configuration#resolusi-ip-klien), batas kepercayaan yang sama diandalkan [`ctx.ip`](/id/by-design/request-id#ip-adalah-sumber-kebenaran). Klien yang tak tepercaya bisa mengatur header apa pun, jadi nilainya tak berarti tanpa batas itu.
54+
55+
## Menyajikan HTTPS Langsung
56+
57+
Server tanpa proxy di depan bisa menterminasi TLS sendiri dengan memberikan sertifikat dan kunci ke [`Deno.serve`](https://docs.deno.com/runtime/fundamentals/http_server/#https-support), runtime di bawah [konfigurasi server](/id/getting-started/server-configuration). Bahkan saat itu redirect dari `http` ke `https` adalah urusan listener terpisah, bukan middleware, jadi aplikasi tetap fokus pada request yang diterimanya.

0 commit comments

Comments
 (0)