@shware/http 3.5.0 → 3.6.0

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.
package/README.md CHANGED
@@ -1,72 +1,164 @@
1
- # Error Handling
1
+ # @shware/http
2
2
 
3
- ## hono example
3
+ HTTP building blocks shared by Shware servers and clients: a Google-style error model (`Status`,
4
+ `StatusError`, `Details`) with client-side parsers, cursor pagination types and helpers, a
5
+ Snowflake ID generator, Hono middlewares (error handler, zod validation, CSRF, authorization, rate
6
+ limiting), Google One Tap sign-in, Standard Webhooks verification and a few small server utilities.
4
7
 
5
- ### With Adapter
8
+ ## Install
6
9
 
7
- ```typescript
8
- import { Hono } from 'hono';
9
- import { HTTPException } from 'hono/http-exception';
10
- import { Status } from '@repo/error';
10
+ ```bash
11
+ pnpm add @shware/http zod
12
+ ```
11
13
 
12
- Status.adapter = (status, response) => {
13
- const headers = { 'Content-Type': 'application/json' };
14
- const res = new Response(JSON.stringify(response), { status, headers });
15
- return new HTTPException(status, { res });
16
- };
14
+ Optional peer dependencies, needed only for the parts that use them:
17
15
 
18
- const app = new Hono();
16
+ ```bash
17
+ pnpm add hono # @shware/http/hono
18
+ pnpm add i18next # types for getErrorMessage()
19
+ ```
19
20
 
20
- app.get('/', () => {
21
- throw Status.invalidArgument('Hello World!').error();
22
- });
21
+ `react` is also declared as an optional peer; no entry point currently requires it.
22
+
23
+ ## Entry points
23
24
 
24
- serve({ fetch: app.fetch, port: 3000 });
25
+ | Import | Contents |
26
+ | ----------------------------- | -------------------------------------------------------------- |
27
+ | `@shware/http` | Error model and parsers, pagination, ids, constants, utilities |
28
+ | `@shware/http/hono` | Hono error handler, zod validators and security middlewares |
29
+ | `@shware/http/google-one-tap` | Browser Google One Tap (FedCM) with an OAuth code fallback |
30
+ | `@shware/http/webhook` | `verifyStandardWebhook()` for Standard Webhooks |
31
+ | `@shware/http/polyfills` | Side-effect import making `BigInt` JSON-serializable |
32
+
33
+ ## Error model
34
+
35
+ Errors follow the [Google API error model](https://cloud.google.com/apis/design/errors): a
36
+ canonical status code mapped to an HTTP status, a message, and a list of typed details.
37
+
38
+ ```json
39
+ {
40
+ "error": {
41
+ "code": 400,
42
+ "status": "INVALID_ARGUMENT",
43
+ "message": "The client specified an invalid argument",
44
+ "details": [{ "@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "INVALID_CODE" }]
45
+ }
46
+ }
25
47
  ```
26
48
 
27
- ### With Global Error Handler
49
+ ### Status
50
+
51
+ Each `Status.*` factory takes an optional message (a default English message is used otherwise)
52
+ and returns a `StatusCode` with three ways to emit it:
53
+
54
+ ```ts
55
+ import { Details, Status } from '@shware/http';
56
+
57
+ // throw it: a StatusError (or whatever Status.adapter returns)
58
+ throw Status.notFound('Team not found').error();
59
+
60
+ // return it: a JSON Response with the matching HTTP status
61
+ return Status.invalidArgument("a batch is one visitor's events").response();
28
62
 
29
- ```typescript
63
+ // just the body: ErrorBody
64
+ const body = Status.internal().body();
65
+ ```
66
+
67
+ | Factory | Status | HTTP |
68
+ | --------------------------- | --------------------- | ---- |
69
+ | `Status.invalidArgument` | `INVALID_ARGUMENT` | 400 |
70
+ | `Status.failedPrecondition` | `FAILED_PRECONDITION` | 400 |
71
+ | `Status.outOfRange` | `OUT_OF_RANGE` | 400 |
72
+ | `Status.unauthorized` | `UNAUTHENTICATED` | 401 |
73
+ | `Status.permissionDenied` | `PERMISSION_DENIED` | 403 |
74
+ | `Status.notFound` | `NOT_FOUND` | 404 |
75
+ | `Status.methodNotAllowed` | `METHOD_NOT_ALLOWED` | 405 |
76
+ | `Status.alreadyExists` | `ALREADY_EXISTS` | 409 |
77
+ | `Status.aborted` | `ABORTED` | 409 |
78
+ | `Status.resourceExhausted` | `RESOURCE_EXHAUSTED` | 429 |
79
+ | `Status.cancelled` | `CANCELLED` | 499 |
80
+ | `Status.unknown` | `UNKNOWN` | 500 |
81
+ | `Status.internal` | `INTERNAL` | 500 |
82
+ | `Status.dataLoss` | `DATA_LOSS` | 500 |
83
+ | `Status.unimplemented` | `UNIMPLEMENTED` | 501 |
84
+ | `Status.unavailable` | `UNAVAILABLE` | 503 |
85
+ | `Status.deadlineExceeded` | `DEADLINE_EXCEEDED` | 504 |
86
+ | `Status.ok` | `OK` | 200 |
87
+
88
+ ### Details
89
+
90
+ `Details.new()` builds the `details` array with chainable methods mirroring
91
+ [`google.rpc` error details](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto):
92
+ `errorInfo`, `retryInfo`, `debugInfo`, `quotaFailure`, `preconditionFailure`, `badRequest`,
93
+ `requestInfo`, `resourceInfo`, `help` and `localizedMessage`.
94
+
95
+ ```ts
96
+ import { Details, Status } from '@shware/http';
97
+
98
+ const details = Details.new()
99
+ .errorInfo({ reason: 'RATE_LIMIT_EXCEEDED' })
100
+ .retryInfo({ retryDelay: 60_000 });
101
+
102
+ throw Status.resourceExhausted('Too many requests, please try again later').error(details);
103
+ ```
104
+
105
+ ### StatusError and Status.adapter
106
+
107
+ By default `.error()` returns a `StatusError` carrying `status` (the HTTP status) and `body` (the
108
+ `ErrorBody`). With Hono, the `errorHandler` from `@shware/http/hono` turns it into a response (see
109
+ below). Without it, handle it yourself:
110
+
111
+ ```ts
112
+ import { Status, StatusError } from '@shware/http';
30
113
  import { Hono } from 'hono';
31
- import { HTTPException } from 'hono/http-exception';
32
- import { Status, StatusError } from '@repo/error';
33
114
 
34
115
  const app = new Hono();
35
116
 
36
117
  app.onError((error, c) => {
37
- if (error instanceof StatusError) {
38
- return c.json(error.response, error.status);
39
- }
40
- return c.json({ error: 'Internal Server Error' }, 500);
118
+ if (error instanceof StatusError) return c.json(error.body, error.status as 400);
119
+ return c.json(Status.internal().body(), 500);
41
120
  });
42
121
 
43
122
  app.get('/', () => {
44
123
  throw Status.invalidArgument('Hello World!').error();
45
124
  });
125
+ ```
46
126
 
47
- serve({ fetch: app.fetch, port: 3000 });
127
+ To have `.error()` produce a framework-native error instead, set `Status.adapter` once at startup:
128
+
129
+ ```ts
130
+ import { Status } from '@shware/http';
131
+ import { HTTPException } from 'hono/http-exception';
132
+
133
+ Status.adapter = (status, body) => {
134
+ const res = Response.json(body, { status });
135
+ return new HTTPException(status as 400, { res });
136
+ };
48
137
  ```
49
138
 
50
- ## Custom ErrorReason
139
+ ### Custom ErrorReason
51
140
 
52
- By default, `ErrorInfo.reason` accepts any `string`. You can use TypeScript [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) to constrain it to a set of known keys:
141
+ By default, `ErrorInfo.reason` accepts any `string`. You can use TypeScript
142
+ [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) to
143
+ constrain it to a set of known keys:
53
144
 
54
- ```typescript
55
- // e.g. shware-http.d.ts
56
- import '@shware/http'; // Required: makes this file a module so that `declare module` augments rather than replaces
145
+ ```ts
146
+ // e.g. src/http.d.ts
147
+ import '@shware/http'; // Required: makes this file a module so `declare module` augments it
57
148
 
58
149
  declare module '@shware/http' {
59
150
  interface ErrorReason {
60
151
  ACCOUNT_BLOCKED: string;
61
- ACCOUNT_LOCKED: string;
152
+ RATE_LIMIT_EXCEEDED: string;
62
153
  SUBSCRIPTION_EXPIRED: string;
63
154
  }
64
155
  }
65
156
  ```
66
157
 
67
- Once declared, `Details.errorInfo({ reason })` will only accept the keys you defined, with full autocomplete support:
158
+ Once declared, `errorInfo({ reason })`, `isErrorReason()` and `getErrorMessage()` only accept or
159
+ return the keys you defined, with autocomplete:
68
160
 
69
- ```typescript
161
+ ```ts
70
162
  import { Details } from '@shware/http';
71
163
 
72
164
  // OK
@@ -76,4 +168,327 @@ Details.new().errorInfo({ reason: 'ACCOUNT_BLOCKED' });
76
168
  Details.new().errorInfo({ reason: 'INVALID' });
77
169
  ```
78
170
 
79
- ## nestjs example
171
+ A convenient pattern is to reuse the keys of your i18n error namespace, so every reason has a
172
+ translation:
173
+
174
+ ```ts
175
+ import '@shware/http';
176
+ import type { ErrorReason as I18nErrorReason } from '@repo/i18n';
177
+
178
+ declare module '@shware/http' {
179
+ interface ErrorReason extends I18nErrorReason {}
180
+ }
181
+ ```
182
+
183
+ `rateLimit` from `@shware/http/hono` emits `RATE_LIMIT_EXCEEDED`; include it if you use it.
184
+
185
+ ### Reading errors on the client
186
+
187
+ The parsers take the decoded response body (`unknown`) and never throw:
188
+
189
+ - `getErrorInfo(data)` returns the `ErrorInfo` detail or `null`.
190
+ - `isErrorReason(data, ...reasons)` checks whether the body carries one of the given reasons.
191
+ - `getFieldViolations(data)` returns the `BadRequest` field violations (`[]` if none).
192
+ - `getErrorMessage(data, t)` returns `{ reason, message }`, preferring a `LocalizedMessage`, then
193
+ `t(reason, metadata)`, then the English message, then `t(status)`, then `t('UNKNOWN')`.
194
+
195
+ ```ts
196
+ import { getErrorInfo, getFieldViolations, isErrorReason } from '@shware/http';
197
+ import { HTTPError } from 'ky';
198
+
199
+ const login = useMutation({
200
+ mutationFn: api.auth.loginEmail,
201
+ meta: { handledReasons: ['INVALID_VERIFICATION_CODE'] },
202
+ onError: (e) => {
203
+ if (!(e instanceof HTTPError)) return;
204
+ if (isErrorReason(e.data, 'INVALID_VERIFICATION_CODE')) {
205
+ setError('verificationCode', { message: t('INVALID_VERIFICATION_CODE', { ns: 'error' }) });
206
+ return;
207
+ }
208
+ getFieldViolations(e.data).forEach(({ field, description }) => {
209
+ setError(field as Path<LoginEmailDTO>, { message: description });
210
+ });
211
+ },
212
+ });
213
+
214
+ // a global handler that skips the reasons a mutation handles itself
215
+ new MutationCache({
216
+ onError: (e, _variables, _context, mutation) => {
217
+ if (!(e instanceof HTTPError)) return;
218
+ if (isErrorReason(e.data, ...(mutation.meta?.handledReasons ?? []))) return;
219
+ const info = getErrorInfo(e.data);
220
+ toast.error(info ? i18n.t(info.reason, { ns: 'error', ...info.metadata }) : 'Unknown error');
221
+ },
222
+ });
223
+ ```
224
+
225
+ ### Client-side error classes
226
+
227
+ `LoginTimeoutError`, `LoginCanceledError` and `CheckoutCreateError` are plain `Error` subclasses
228
+ for sign-in and checkout flows; `PurchaseError` is a const enum of purchase failure codes
229
+ (`LOGIN_TIMEOUT`, `LOGIN_CANCELED`, `CHECKOUT_CANCELED`, `CHECKOUT_CREATE_FAILED`,
230
+ `CHECKOUT_SESSION_ID_NOT_FOUND`, `UNKNOWN`).
231
+
232
+ ## Hono (`@shware/http/hono`)
233
+
234
+ | Export | Description |
235
+ | -------------- | -------------------------------------------------------------------- |
236
+ | `errorHandler` | `app.onError` handler turning any error into an `ErrorBody` response |
237
+ | `zValidator` | zod / zod mini validator; failures become `INVALID_ARGUMENT` |
238
+ | `zBeaconJson` | `json` validator that also accepts `text/plain` beacon bodies |
239
+ | `bigintId` | zod mini schema parsing a decimal string into a `bigint` |
240
+ | `csrf` | Double-submit cookie CSRF check (`XSRF-TOKEN` / `X-XSRF-TOKEN`) |
241
+ | `authorizer` | Requires authentication on the paths matching `rules` |
242
+ | `rateLimit` | Token-bucket rate limiter backed by any async key-value store |
243
+ | `geolocation` | Visitor geolocation from Cloudflare, Vercel or CloudFront headers |
244
+
245
+ ### Wiring an app
246
+
247
+ `errorHandler` reads `c.get('requestId')`, so register Hono's `requestId()` middleware. Every
248
+ error response gets a `RequestInfo` detail; 5xx are logged with `console.error`, 4xx with
249
+ `console.warn`.
250
+
251
+ ```ts
252
+ import '@shware/http/polyfills';
253
+ import { Status } from '@shware/http';
254
+ import { type CSRFConfig, authorizer, csrf, errorHandler } from '@shware/http/hono';
255
+ import { OAuth2Error } from '@shware/security';
256
+ import { Hono } from 'hono';
257
+ import { requestId } from 'hono/request-id';
258
+
259
+ const csrfConfig: CSRFConfig = {
260
+ origin: ['https://app.example.com'],
261
+ secFetchSite: ['same-origin', 'same-site', 'none'],
262
+ ignores: [
263
+ '/login/oauth2/code/*',
264
+ '/v1/webhooks/*',
265
+ { path: '/v1/analytics/*', methods: ['POST'] },
266
+ ],
267
+ };
268
+
269
+ const app = new Hono();
270
+
271
+ app.onError((error, c) => {
272
+ // map third-party errors to a Status before delegating
273
+ if (error instanceof OAuth2Error) return errorHandler(Status.unauthorized().error(), c);
274
+ return errorHandler(error, c);
275
+ });
276
+
277
+ app.use(requestId());
278
+ app.use(csrf(csrfConfig));
279
+ app.use(
280
+ authorizer({
281
+ auth, // { isAuthenticated: (request: Request) => Promise<boolean> }
282
+ rules: ['/v1/user/*', '/v1/credits/*', { path: '/v1/analytics/links', methods: ['POST'] }],
283
+ })
284
+ );
285
+ ```
286
+
287
+ `csrf` lets safe methods (`GET`, `HEAD`, `OPTIONS` by default), allowed `origin`s, allowed
288
+ `Sec-Fetch-Site` values and `ignores` through; otherwise it compares the cookie and header tokens
289
+ and throws `PERMISSION_DENIED`. `authorizer` skips `OPTIONS` and throws `UNAUTHENTICATED` for
290
+ matching paths when `auth.isAuthenticated()` is false.
291
+
292
+ ### Validation
293
+
294
+ ```ts
295
+ import { pageParamsSchema } from '@shware/http';
296
+ import { bigintId, geolocation, zBeaconJson, zValidator } from '@shware/http/hono';
297
+ import { object } from 'zod/mini';
298
+
299
+ app.get('/v1/credits/usages', zValidator('query', pageParamsSchema()), async (c) => {
300
+ const { limit, prev, next } = c.req.valid('query');
301
+ // ...
302
+ });
303
+
304
+ app.get('/v1/teams/:id', zValidator('param', object({ id: bigintId })), async (c) => {
305
+ const { id } = c.req.valid('param'); // bigint
306
+ // ...
307
+ });
308
+
309
+ app.post('/v1/analytics/events', zBeaconJson(createTrackEventSchema), async (c) => {
310
+ const events = c.req.valid('json');
311
+ const geo = geolocation(c); // { ip_address, country, city, region, time_zone, ... }
312
+ // ...
313
+ });
314
+ ```
315
+
316
+ ### Rate limiting
317
+
318
+ ```ts
319
+ import { rateLimit } from '@shware/http/hono';
320
+
321
+ app.use(
322
+ '/v1/auth/*',
323
+ rateLimit({
324
+ kv, // { getItem, setItem(key, value, expiresIn?), removeItem }
325
+ rate: 1, // tokens refilled per second
326
+ capacity: 10,
327
+ // optional: getIdentifier (defaults to client IP), skip, onRateLimited, keyPrefix, expiresIn
328
+ })
329
+ );
330
+ ```
331
+
332
+ It sets `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; when exhausted it
333
+ adds `Retry-After` and answers `RESOURCE_EXHAUSTED` with `RATE_LIMIT_EXCEEDED` and `RetryInfo`.
334
+
335
+ ## Pagination
336
+
337
+ Cursor pagination types (`Page<T>`, `PageParams`, `Response<T>`, `Empty`, ...) and helpers shared
338
+ by the API and its clients.
339
+
340
+ Server side, `pageParamsSchema(max = 100, defaultLimit = 20)` validates `limit`, `prev` and
341
+ `next`, and `Cursor` encodes ids into opaque base64 tokens:
342
+
343
+ ```ts
344
+ import { Cursor, type Page, pageParamsSchema } from '@shware/http';
345
+ import { zValidator } from '@shware/http/hono';
346
+ import { hasText } from '@shware/utils';
347
+
348
+ app.get('/v1/credits/usages', zValidator('query', pageParamsSchema()), async (c) => {
349
+ const { prev, next, limit } = c.req.valid('query');
350
+ const before = hasText(next) ? Cursor.decode(next, 'string') : undefined;
351
+ const list = await findUsages({ before, limit: limit + 1 });
352
+
353
+ const hasMore = list.length > limit;
354
+ const data = hasMore ? list.slice(0, limit) : list;
355
+ const paging = hasMore ? Cursor.of(data.at(0)?.id, data.at(-1)?.id) : Cursor.empty();
356
+ return c.json<Page<CreditUsage>>({ data, paging });
357
+ });
358
+ ```
359
+
360
+ Client side, the page param helpers plug straight into TanStack Query:
361
+
362
+ ```ts
363
+ import { getNextPageParam, getPreviousPageParam, initialPageParam } from '@shware/http';
364
+
365
+ const query = useInfiniteQuery({
366
+ queryKey: ['charges'],
367
+ initialPageParam,
368
+ getNextPageParam: getNextPageParam<Charge>,
369
+ getPreviousPageParam: getPreviousPageParam<Charge>,
370
+ queryFn: ({ pageParam }) => api.purchase.listCharges({ limit: 20, ...pageParam }),
371
+ });
372
+ ```
373
+
374
+ `Pages` (`flatten`, `dedupe`, `prepend`, `append`, `update`, `updateById`, `remove`,
375
+ `removeById`) and `Items` (`push`, `unshift`, `pushAndDedupe`, `unshiftAndDedupe`, `update`,
376
+ `updateById`, `remove`, `removeById`) return updater functions for `setQueryData` on infinite and
377
+ plain list queries:
378
+
379
+ ```ts
380
+ import { Pages } from '@shware/http';
381
+
382
+ queryClient.setQueryData(['charges'], Pages.removeById<Charge>(id));
383
+ ```
384
+
385
+ ## Google One Tap (`@shware/http/google-one-tap`)
386
+
387
+ Browser-only. `prompt()` loads the Google Identity Services script and shows One Tap via FedCM. It
388
+ never rejects; when One Tap cannot run (`fedcm_unsupported`, `script_load_failed`,
389
+ `prompt_error`) fall back to `requestCode()`, an OAuth 2.0 authorization-code popup that must be
390
+ called from a user gesture.
391
+
392
+ ```ts
393
+ import { prompt, requestCode } from '@shware/http/google-one-tap';
394
+
395
+ const result = await prompt({ client_id: GOOGLE_CLIENT_ID });
396
+
397
+ if (result.authorized) {
398
+ await api.auth.loginOAuth2Onetap(result.credential); // { credential: ID token, select_by }
399
+ } else if ('unsupported' in result) {
400
+ // later, on a button click:
401
+ const code = await requestCode({ client_id: GOOGLE_CLIENT_ID });
402
+ if (code.ok) await exchangeCodeOnBackend(code.response.code);
403
+ }
404
+ ```
405
+
406
+ ## Webhooks (`@shware/http/webhook`)
407
+
408
+ `verifyStandardWebhook<T>(headers, payload, secret)` verifies the `webhook-id`,
409
+ `webhook-timestamp` (5 minute tolerance) and `webhook-signature` headers against a base64 secret
410
+ and returns the parsed payload, or throws `INVALID_ARGUMENT`. Node.js `crypto` is required.
411
+
412
+ ```ts
413
+ import { verifyStandardWebhook } from '@shware/http/webhook';
414
+
415
+ app.post('/v1/webhooks/acme', async (c) => {
416
+ const event = verifyStandardWebhook<AcmeEvent>(c.req.header(), await c.req.text(), secret);
417
+ // ...
418
+ return c.body(null, 204);
419
+ });
420
+ ```
421
+
422
+ ## Polyfills (`@shware/http/polyfills`)
423
+
424
+ Import once at the entry point so `bigint` values (e.g. Snowflake ids) serialize as strings in
425
+ `JSON.stringify` and `c.json()` instead of throwing:
426
+
427
+ ```ts
428
+ import '@shware/http/polyfills';
429
+ ```
430
+
431
+ ## Other utilities
432
+
433
+ ### Snowflake ids
434
+
435
+ `uid` is a ready-made `UidGenerator` with a random machine id; create your own with
436
+ `new UidGenerator(machineId)` (10-bit machine id, 12-bit sequence, epoch 2020-01-01).
437
+
438
+ ```ts
439
+ import { uid } from '@shware/http';
440
+
441
+ const id: bigint = uid.next();
442
+ ```
443
+
444
+ ### Column lengths
445
+
446
+ `MAX_LENGTH` holds maximum lengths for common identifiers, handy for database columns and
447
+ validators: `UUID`, `DEVICE_ID`, `ADVERTISING_ID`, `IP`, `TIMEZONE`, `COUNTRY_CODE`,
448
+ `LANGUAGE_CODE`, `EMAIL`, `PHONE_NUMBER` and more.
449
+
450
+ ```ts
451
+ import { MAX_LENGTH } from '@shware/http';
452
+
453
+ const email = varchar('email', { length: MAX_LENGTH.EMAIL }); // 320
454
+ ```
455
+
456
+ ### Countries
457
+
458
+ `ISO_3601_1` is the list of ISO 3166-1 countries (`{ numeric, name, alpha2, alpha3 }`, typed as
459
+ `ISO3166CountryCode`).
460
+
461
+ ```ts
462
+ import { ISO_3601_1 } from '@shware/http';
463
+
464
+ app.get('/v1/iso/countries', (c) => c.json({ data: ISO_3601_1 }));
465
+ ```
466
+
467
+ ### Geolocation and Google Tag Gateway
468
+
469
+ `getGeolocation(request)` reads visitor location headers from Vercel, Cloudflare or CloudFront.
470
+ `forwardToGoogleTagGateway(request, gaId)` proxies a first-party measurement path to Google Tag
471
+ Gateway, forwarding cookies, client hints and geolocation:
472
+
473
+ ```ts
474
+ import { forwardToGoogleTagGateway } from '@shware/http';
475
+
476
+ // e.g. a catch-all route at /metrics/*
477
+ const proxy = ({ request }: { request: Request }) => forwardToGoogleTagGateway(request, GA_ID);
478
+ ```
479
+
480
+ ### Server-Timing
481
+
482
+ `timing()` collects metrics and writes `Server-Timing` (and `Timing-Allow-Origin`) headers:
483
+
484
+ ```ts
485
+ import { timing } from '@shware/http';
486
+
487
+ const t = timing();
488
+ t.startTime('db', 'Database');
489
+ const rows = await query();
490
+ t.endTime('db');
491
+
492
+ const response = Response.json(rows);
493
+ t.setTiming(response);
494
+ ```
@@ -7,4 +7,5 @@ interface Props<A extends unknown[], R> {
7
7
  }
8
8
  export declare function cache<A extends unknown[], R>(_props: Props<A, R>): void;
9
9
  //#endregion
10
+ export {};
10
11
  //# sourceMappingURL=index.d.cts.map
@@ -7,4 +7,5 @@ interface Props<A extends unknown[], R> {
7
7
  }
8
8
  export declare function cache<A extends unknown[], R>(_props: Props<A, R>): void;
9
9
  //#endregion
10
+ export {};
10
11
  //# sourceMappingURL=index.d.mts.map
@@ -153,4 +153,5 @@ export interface GoogleAccounts {
153
153
  };
154
154
  }
155
155
  //#endregion
156
+ export {};
156
157
  //# sourceMappingURL=types.d.cts.map
@@ -153,4 +153,5 @@ export interface GoogleAccounts {
153
153
  };
154
154
  }
155
155
  //#endregion
156
+ export {};
156
157
  //# sourceMappingURL=types.d.mts.map
@@ -15,4 +15,5 @@ export interface AuthorizerConfig {
15
15
  }
16
16
  export declare function authorizer({ auth, errorMessage, rules }: AuthorizerConfig): MiddlewareHandler;
17
17
  //#endregion
18
+ export {};
18
19
  //# sourceMappingURL=authorizer.d.cts.map
@@ -15,4 +15,5 @@ export interface AuthorizerConfig {
15
15
  }
16
16
  export declare function authorizer({ auth, errorMessage, rules }: AuthorizerConfig): MiddlewareHandler;
17
17
  //#endregion
18
+ export {};
18
19
  //# sourceMappingURL=authorizer.d.mts.map
@@ -72,4 +72,5 @@ export interface CSRFConfig {
72
72
  */
73
73
  export declare function csrf(config?: CSRFConfig): MiddlewareHandler;
74
74
  //#endregion
75
+ export {};
75
76
  //# sourceMappingURL=csrf.d.cts.map
@@ -72,4 +72,5 @@ export interface CSRFConfig {
72
72
  */
73
73
  export declare function csrf(config?: CSRFConfig): MiddlewareHandler;
74
74
  //#endregion
75
+ export {};
75
76
  //# sourceMappingURL=csrf.d.mts.map
@@ -35,4 +35,5 @@ export declare function isAxiosError(payload: unknown): payload is AxiosError;
35
35
  export declare function isStatusError(payload: unknown): payload is StatusError;
36
36
  export declare function errorHandler<E extends Env = never>(error: Error | HTTPResponseError, c: Context<E>): Response | Promise<Response>;
37
37
  //#endregion
38
+ export {};
38
39
  //# sourceMappingURL=handler.d.cts.map
@@ -35,4 +35,5 @@ export declare function isAxiosError(payload: unknown): payload is AxiosError;
35
35
  export declare function isStatusError(payload: unknown): payload is StatusError;
36
36
  export declare function errorHandler<E extends Env = never>(error: Error | HTTPResponseError, c: Context<E>): Response | Promise<Response>;
37
37
  //#endregion
38
+ export {};
38
39
  //# sourceMappingURL=handler.d.mts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/http",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -66,20 +66,19 @@
66
66
  "access": "public"
67
67
  },
68
68
  "dependencies": {
69
- "@shware/utils": "^1.6.2",
70
- "vitest": "^5.0.2",
71
- "zod": "^4.6.5"
69
+ "@shware/utils": "^1.6.3"
72
70
  },
73
71
  "devDependencies": {
74
72
  "@repo/typescript-config": "0.0.0",
75
- "@types/node": "^26.6.3",
76
- "@types/react": "^19.3.0",
77
- "typescript": "^7.0.2"
73
+ "@types/node": "^26.6.5",
74
+ "typescript": "^7.0.2",
75
+ "vitest": "^5.0.5",
76
+ "zod": "^4.6.7"
78
77
  },
79
78
  "peerDependencies": {
80
79
  "hono": "^4.13.9",
81
80
  "i18next": "^26.4.2",
82
- "react": "^19.3.0"
81
+ "zod": "^4.6.5"
83
82
  },
84
83
  "peerDependenciesMeta": {
85
84
  "hono": {
@@ -87,9 +86,6 @@
87
86
  },
88
87
  "i18next": {
89
88
  "optional": true
90
- },
91
- "react": {
92
- "optional": true
93
89
  }
94
90
  },
95
91
  "scripts": {