@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  5. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  6. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  7. package/dist/chunk-GVNLZZTN.js.map +1 -0
  8. package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
  9. package/dist/chunk-SPQUX3C7.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,533 @@
1
+ # HTTP requests
2
+
3
+ The `request()` helper gives you the current HTTP request: its URL, headers and input, the files it uploaded and the
4
+ cookies it sent.
5
+
6
+ ## Interacting with the request
7
+
8
+ ### Accessing the request
9
+
10
+ Call `request()` anywhere in your code while a request is handled: in a route, a controller, a middleware or a view.
11
+ It returns the same object each time for the same request:
12
+
13
+ ```ts title="app/Http/Controllers/UserController.ts"
14
+ import { redirect, request } from '@orieljs/framework'
15
+ import { Controller } from './Controller'
16
+
17
+ export class UserController extends Controller {
18
+ async store() {
19
+ const name = request().input<string>('name')
20
+
21
+ // Store the user...
22
+
23
+ return redirect('/users')
24
+ }
25
+ }
26
+ ```
27
+
28
+ The same goes for a route's closure:
29
+
30
+ ```ts
31
+ Route.get('/', () => {
32
+ const page = request().query('page', '1')
33
+
34
+ return `Page ${page}`
35
+ })
36
+ ```
37
+
38
+ `request('name')` is a short form of `request().input('name')`, and takes a default too.
39
+
40
+ > [!NOTE]
41
+ > Laravel injects the request into a closure or a controller method that type-hints it. TypeScript types are gone
42
+ > when your app runs, so Oriel can't see them: an action receives its route parameters and nothing else, and reads the
43
+ > request from `request()`.
44
+
45
+ #### Route parameters
46
+
47
+ A route's parameters are still the action's arguments, in the order of the URI:
48
+
49
+ ```ts
50
+ Route.put('/user/{id}', [UserController, 'update'])
51
+ ```
52
+
53
+ ```ts title="app/Http/Controllers/UserController.ts"
54
+ import { redirect, request } from '@orieljs/framework'
55
+ import { User } from '../../Models/User'
56
+ import { Controller } from './Controller'
57
+
58
+ export class UserController extends Controller {
59
+ async update(id: string) {
60
+ const user = await User.findOrFail(id)
61
+
62
+ await user.update({ name: request().input<string>('name') })
63
+
64
+ return redirect('/users')
65
+ }
66
+ }
67
+ ```
68
+
69
+ `request().route('id')` reads one of them from anywhere else, after [model binding](routing.md#route-model-binding),
70
+ and `request().route()` gives them all.
71
+
72
+ ### Request path, host, and method
73
+
74
+ #### Retrieving the request path
75
+
76
+ `path()` returns the request's path without the slashes at its ends. For `http://example.com/foo/bar`, it is
77
+ `foo/bar`; for the home page, `/`:
78
+
79
+ ```ts
80
+ const uri = request().path()
81
+ ```
82
+
83
+ #### Inspecting the request path and route
84
+
85
+ `is()` checks the path against one or more patterns, where `*` matches anything:
86
+
87
+ ```ts
88
+ if (request().is('admin/*')) {
89
+ // ...
90
+ }
91
+ ```
92
+
93
+ `routeIs()` checks the name of the route the request matched, the same way:
94
+
95
+ ```ts
96
+ if (request().routeIs('admin.*')) {
97
+ // ...
98
+ }
99
+ ```
100
+
101
+ #### Retrieving the request URL
102
+
103
+ `url()` returns the URL without its query string, and `fullUrl()` with it:
104
+
105
+ ```ts
106
+ const url = request().url()
107
+
108
+ const urlWithQueryString = request().fullUrl()
109
+ ```
110
+
111
+ #### Retrieving the request host
112
+
113
+ `root()` returns the scheme and the host, as `https://example.com`, and `secure()` tells you whether the request came
114
+ over HTTPS. Behind a proxy, both follow the proxy's `X-Forwarded-*` headers once you
115
+ [trust it](#configuring-trusted-proxies).
116
+
117
+ #### Retrieving the request method
118
+
119
+ `method()` returns the request's verb, upper-cased, and `isMethod()` compares it in any case:
120
+
121
+ ```ts
122
+ const method = request().method()
123
+
124
+ if (request().isMethod('post')) {
125
+ // ...
126
+ }
127
+ ```
128
+
129
+ A form's `_method` field, or an `X-HTTP-Method-Override` header, changes the method of a `POST`: see
130
+ [form method spoofing](routing.md#form-method-spoofing).
131
+
132
+ ### Request headers
133
+
134
+ `header()` returns a header, by its name in any case, or `undefined` when the request doesn't have it. A second
135
+ argument is the default:
136
+
137
+ ```ts
138
+ const value = request().header('X-Header-Name')
139
+
140
+ const withDefault = request().header('X-Header-Name', 'default')
141
+ ```
142
+
143
+ `bearerToken()` returns the token of an `Authorization: Bearer` header, or `undefined`, and `userAgent()` the
144
+ `User-Agent` header:
145
+
146
+ ```ts
147
+ const token = request().bearerToken()
148
+ ```
149
+
150
+ ### Request IP address
151
+
152
+ `ip()` returns the address of the client that sent the request, or `undefined` when the server doesn't know it:
153
+
154
+ ```ts
155
+ const ipAddress = request().ip()
156
+ ```
157
+
158
+ Behind a [trusted proxy](#configuring-trusted-proxies), it is the client's address from `X-Forwarded-For`. Treat an
159
+ IP address as information the client controls.
160
+
161
+ ### Content negotiation
162
+
163
+ `expectsJson()` tells you whether the request wants a JSON response: its `Accept` header asks for JSON first, or it
164
+ is an AJAX request that accepts anything. `wantsJson()` checks the `Accept` header alone, and `ajax()` the
165
+ `X-Requested-With: XMLHttpRequest` header:
166
+
167
+ ```ts
168
+ if (request().expectsJson()) {
169
+ // ...
170
+ }
171
+ ```
172
+
173
+ The exception handler uses `expectsJson()` to choose between an HTML error page and JSON: see
174
+ [Error handling](errors.md).
175
+
176
+ ### The web-standard request
177
+
178
+ `request().raw` is the web-standard `Request` the server received. Oriel reads JSON, URL-encoded and multipart bodies
179
+ for you; a body of any other type is left unread, so read it from `raw`:
180
+
181
+ ```ts
182
+ Route.post('/hooks/raw', async () => {
183
+ const bytes = await request().raw.arrayBuffer()
184
+
185
+ return { received: bytes.byteLength }
186
+ })
187
+ ```
188
+
189
+ > [!WARNING]
190
+ > A body you read from `raw` isn't held to the size limit of the bodies Oriel reads (see
191
+ > [body size limits](#body-size-limits)): check its size yourself.
192
+
193
+ ## Input
194
+
195
+ ### Retrieving input
196
+
197
+ #### Retrieving all input data
198
+
199
+ `all()` returns the request's input, from the query string and the body, with its uploaded files:
200
+
201
+ ```ts
202
+ const input = request().all()
203
+ ```
204
+
205
+ #### Retrieving an input value
206
+
207
+ `input()` returns one value, whatever the request's verb, from the body or, when the body hasn't the key, from the
208
+ query string. A second argument is the default when the key is missing:
209
+
210
+ ```ts
211
+ const name = request().input('name')
212
+
213
+ const withDefault = request().input('name', 'Sally')
214
+ ```
215
+
216
+ Form fields named with brackets, such as `products[0][name]`, arrive nested. Reach into them with dots:
217
+
218
+ ```ts
219
+ const name = request().input('products.0.name')
220
+ ```
221
+
222
+ `input()` with no key returns every value, without the files.
223
+
224
+ `input()` returns `unknown` unless you give it a type, as in `input<string>('name')`. That type is a cast, not a
225
+ check: the client can send anything, so [validate](validation.md) the input you rely on.
226
+
227
+ #### Retrieving input from the query string
228
+
229
+ `query()` reads the query string only, with the same defaults, and with no key returns it all:
230
+
231
+ ```ts
232
+ const name = request().query('name')
233
+
234
+ const withDefault = request().query('name', 'Helen')
235
+
236
+ const query = request().query()
237
+ ```
238
+
239
+ #### Retrieving JSON input values
240
+
241
+ A request with a JSON body, sent with a `Content-Type` of `application/json` (or any type that ends in `+json`), has
242
+ the JSON as its input, so `input()` reads it with dots too:
243
+
244
+ ```ts
245
+ const name = request().input('user.name')
246
+ ```
247
+
248
+ `content()` returns the raw text of a JSON or URL-encoded body. A body that isn't valid JSON reads as empty input.
249
+
250
+ #### Retrieving stringable input values
251
+
252
+ `string()` returns a value as a [`Stringable`](helpers.md#fluent-strings), so you can chain string methods onto it. A
253
+ missing value is the empty string, or the default you give:
254
+
255
+ ```ts
256
+ const name = request().string('name').trim().toString()
257
+ ```
258
+
259
+ `str()` is the same method under a shorter name.
260
+
261
+ #### Retrieving integer input values
262
+
263
+ `integer()` reads a value as a whole number, from its leading digits, as PHP's `intval` does. A missing value gives
264
+ the default (`0` unless you give one), and text that doesn't start with a number gives `0`:
265
+
266
+ ```ts
267
+ const perPage = request().integer('per_page', 15)
268
+ ```
269
+
270
+ #### Retrieving boolean input values
271
+
272
+ Checkboxes and other fields send truthy text, such as `on`. `boolean()` returns `true` for `1`, `'1'`, `true`,
273
+ `'true'`, `'on'` and `'yes'`, in any case, and `false` for everything else. A missing value gives the default
274
+ (`false` unless you give one):
275
+
276
+ ```ts
277
+ const archived = request().boolean('archived')
278
+ ```
279
+
280
+ #### Retrieving a portion of the input data
281
+
282
+ `only()` and `except()` take a list of keys, or the keys as arguments, and accept dots:
283
+
284
+ ```ts
285
+ const credentials = request().only(['username', 'password'])
286
+
287
+ const sameCredentials = request().only('username', 'password')
288
+
289
+ const withoutCard = request().except(['credit_card'])
290
+
291
+ const withoutCardEither = request().except('credit_card')
292
+ ```
293
+
294
+ > [!WARNING]
295
+ > `only()` returns the keys that are present: a key the request didn't send isn't in its result, not even as
296
+ > `undefined`.
297
+
298
+ ### Input presence
299
+
300
+ `has()` tells you whether every key you give is present, even with an empty value, and `hasAny()` whether one of them
301
+ is:
302
+
303
+ ```ts
304
+ if (request().has('name')) {
305
+ // ...
306
+ }
307
+
308
+ if (request().has(['name', 'email'])) {
309
+ // ...
310
+ }
311
+
312
+ if (request().hasAny(['name', 'email'])) {
313
+ // ...
314
+ }
315
+ ```
316
+
317
+ `filled()` tells you whether a value is present and not blank: not `null`, not text made only of spaces, not an empty
318
+ list or object. `missing()` is the opposite of `has()`:
319
+
320
+ ```ts
321
+ if (request().filled('name')) {
322
+ // ...
323
+ }
324
+
325
+ if (request().missing('name')) {
326
+ // ...
327
+ }
328
+ ```
329
+
330
+ ### Merging additional input
331
+
332
+ `merge()` adds values to the request's input, replacing those it already has:
333
+
334
+ ```ts
335
+ request().merge({ votes: 0 })
336
+ ```
337
+
338
+ ### Old input
339
+
340
+ Oriel can keep a request's input for the next request, so a form can show what the user typed after a failed
341
+ validation. When validation fails, Oriel does this for you, and leaves out the password fields.
342
+
343
+ #### Flashing input then redirecting
344
+
345
+ `withInput()` on a redirect flashes the input to the session. It takes the input to flash, or flashes all of it:
346
+
347
+ ```ts
348
+ const back = redirect('/form').withInput()
349
+
350
+ const toRoute = redirect().route('user.create').withInput()
351
+
352
+ const withoutPassword = redirect('/form').withInput(request().except('password'))
353
+ ```
354
+
355
+ #### Retrieving old input
356
+
357
+ `old()` returns a value the previous request flashed, or the default you give. In a TSX view, it fills a field
358
+ again:
359
+
360
+ ```tsx title="resources/views/user/create.tsx"
361
+ import { csrfField, old } from '@orieljs/framework'
362
+
363
+ export default function Create() {
364
+ return (
365
+ <form method="post" action="/users">
366
+ {csrfField()}
367
+ <input type="text" name="username" value={old('username', '')} />
368
+ <button type="submit">Create</button>
369
+ </form>
370
+ )
371
+ }
372
+ ```
373
+
374
+ `old()` works in the rest of your code too, as does `request().session().getOldInput('username')`. Laravel's
375
+ `$request->flash()`, `flashOnly()` and `flashExcept()` aren't on Oriel's request: flash input with `withInput()`, or
376
+ with the session's `flashInput()`.
377
+
378
+ ### Cookies
379
+
380
+ #### Retrieving cookies from requests
381
+
382
+ The cookies Oriel sets are encrypted, and a cookie that doesn't decrypt is dropped, so the client can't change one.
383
+ `cookie()` returns a cookie's decrypted value, or `undefined`:
384
+
385
+ ```ts
386
+ const value = request().cookie('name')
387
+ ```
388
+
389
+ `Cookie.get('name')` reads the same value through the `Cookie` [facade](facades.md), and `request().cookies()` returns
390
+ them all. [Responses](responses.md) covers setting cookies.
391
+
392
+ Cookies are decrypted by the `web` middleware group: in `routes/api.ts`, `cookie()` returns them as the client sent
393
+ them.
394
+
395
+ A cookie your scripts write isn't encrypted, so `EncryptCookies` drops it as one that doesn't decrypt. List its name
396
+ with `EncryptCookies.except('appearance')` in `bootstrap/app.ts` to read it as it is, as the starter kits do.
397
+
398
+ ## Input trimming and normalization
399
+
400
+ Two global middleware tidy the input of every request before your code sees it, as in Laravel: `TrimStrings` trims
401
+ every text value, except the `password`, `password_confirmation` and `current_password` fields, and
402
+ `ConvertEmptyStringsToNull` turns empty text into `null`. They apply to the query string and to the body, JSON
403
+ included. They always run: Laravel's switches to turn them off, for every request or some, aren't in Oriel.
404
+
405
+ ## Files
406
+
407
+ ### Retrieving uploaded files
408
+
409
+ `file()` returns an uploaded file from a multipart form, or `undefined`, and `hasFile()` tells you whether there is
410
+ one. A file field left empty doesn't count:
411
+
412
+ ```ts
413
+ const file = request().file('photo')
414
+
415
+ if (request().hasFile('photo')) {
416
+ // ...
417
+ }
418
+ ```
419
+
420
+ `allFiles()` returns every uploaded file, by field name.
421
+
422
+ #### File names and extensions
423
+
424
+ An uploaded file has the name, the MIME type and the size the browser gave it, in `clientName`, `mimeType` and
425
+ `size`. `extension()` guesses the extension from the file's content, so it can differ from the one in its name, which
426
+ `clientExtension()` returns:
427
+
428
+ ```ts
429
+ declare const file: UploadedFile
430
+ // ---cut---
431
+ const extension = file.extension() // 'png', from the bytes
432
+ const clientExtension = file.clientExtension() // 'jpg', from photo.jpg
433
+ ```
434
+
435
+ `extension()` reads the first bytes of the file. When they show a known format, its extension wins. Otherwise the
436
+ client's claim is kept, unless it names a format the bytes would have shown, or a type a browser renders or runs,
437
+ such as `html` or `svg`; then plain text is `txt`, and anything else has no extension.
438
+
439
+ #### Other file methods
440
+
441
+ `hashName()` returns a random 40-character name with that extension, of the kind `store()` gives the file, and a new
442
+ one each time you call it. `arrayBuffer()`, `text()` and `stream()` read the file's content:
443
+
444
+ ```ts
445
+ declare const file: UploadedFile
446
+ // ---cut---
447
+ const name = file.hashName() // 'Xb3bTx...Q9.png'
448
+ const text = await file.text()
449
+ const stream = file.stream()
450
+ ```
451
+
452
+ ### Storing uploaded files
453
+
454
+ `store()` saves an uploaded file on one of your filesystem disks, in the folder you name, under a new random name of the
455
+ kind `hashName()` makes. It returns the file's path on the disk, such as `images/<name>.png`. A second argument names
456
+ the disk; without it, the default disk is used:
457
+
458
+ ```ts
459
+ declare const file: UploadedFile
460
+ // ---cut---
461
+ const path = await file.store('images')
462
+
463
+ const onS3 = await file.store('images', 's3')
464
+ ```
465
+
466
+ `storeAs()` takes the file's name as well:
467
+
468
+ ```ts
469
+ declare const file: UploadedFile
470
+ // ---cut---
471
+ const path = await file.storeAs('images', 'filename.jpg')
472
+
473
+ const onS3 = await file.storeAs('images', 'filename.jpg', 's3')
474
+ ```
475
+
476
+ Disks are configured in `config/filesystems.ts`; [File storage](storage.md) covers them.
477
+
478
+ > [!NOTE]
479
+ > Laravel's uploaded files have `isValid()`, which checks that the upload succeeded, and `path()`, the path of the
480
+ > temporary file PHP wrote. Oriel's have neither: an upload is read into memory with the rest of the body, so a file
481
+ > `file()` returns has arrived whole, and has no path until you store it.
482
+
483
+ ## Body size limits
484
+
485
+ Oriel reads a JSON, URL-encoded or multipart body into memory before your code runs, and refuses a body larger than
486
+ `http.max_body_size`: 8 MB unless you change it. A larger body gets a 413 ("Content Too Large"), and when the
487
+ `Content-Length` header gives the size, the body isn't read at all. The limit is the same for every route.
488
+
489
+ A JSON body can also nest objects and arrays only so deep: past `http.max_json_depth`, 512 levels unless you change
490
+ it, the request gets a 400 ("The request body is nested too deeply.").
491
+
492
+ Raise them in `config/http.ts`, where `max_body_size` is a number of bytes, and `null` turns either limit off:
493
+
494
+ ```ts title="config/http.ts"
495
+ export default {
496
+ trusted_proxies: [],
497
+ csrf_except: [],
498
+ redirect_guests_to: '/login',
499
+ max_body_size: 20 * 1024 * 1024,
500
+ max_json_depth: 512,
501
+ }
502
+ ```
503
+
504
+ > [!WARNING]
505
+ > The limit counts the whole body, files included. To accept uploads larger than 8 MB, such as the 12 MB
506
+ > [Live's uploads](live.md#file-uploads) allow, raise `max_body_size` above the largest you accept.
507
+
508
+ ## Configuring trusted proxies
509
+
510
+ Behind a load balancer or a proxy that ends TLS, your app sees the proxy's request, over HTTP, from the proxy's
511
+ address. Tell Oriel which proxies to trust with `trusted_proxies` in `config/http.ts`: exact addresses, IPv4 ranges
512
+ such as `10.0.0.0/8`, or `*` for any:
513
+
514
+ ```ts title="config/http.ts"
515
+ export default {
516
+ trusted_proxies: ['192.168.1.1', '10.0.0.0/8'],
517
+ csrf_except: [],
518
+ redirect_guests_to: '/login',
519
+ max_body_size: 8 * 1024 * 1024,
520
+ }
521
+ ```
522
+
523
+ For a request from a trusted proxy, `ip()` reads the client's address from `X-Forwarded-For`, and the scheme and
524
+ host come from `X-Forwarded-Proto` and `X-Forwarded-Host`, so `url()`, `route()` and `secure()` match what the
525
+ browser asked for. The list may also be a comma-separated string, so it can come from your `.env`:
526
+ `trusted_proxies: env('TRUSTED_PROXIES', '')`.
527
+
528
+ #### Trusting all proxies
529
+
530
+ When you can't know your load balancers' addresses, as with many cloud providers, trust them all with `'*'`. Do it
531
+ only when nothing can reach your app except through them: any client could then set the `X-Forwarded-*` headers.
532
+
533
+ Laravel's trusted hosts (`trustHosts()`) aren't in Oriel: let your web server or load balancer refuse unknown hosts.