@alxia/react-router 0.40.0 → 0.42.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.
Files changed (49) hide show
  1. package/README.md +91 -2
  2. package/dist/chunks/index-1f16k1sj.js +186 -0
  3. package/dist/chunks/index-1f16k1sj.js.map +14 -0
  4. package/dist/chunks/index-atxcmy0f.js +47 -0
  5. package/dist/chunks/index-atxcmy0f.js.map +11 -0
  6. package/dist/chunks/index-gchzf3mc.js +30 -0
  7. package/dist/chunks/index-gchzf3mc.js.map +10 -0
  8. package/dist/chunks/{index-2swe47m6.js → index-hn6htqc2.js} +3 -46
  9. package/dist/chunks/{index-2swe47m6.js.map → index-hn6htqc2.js.map} +4 -6
  10. package/dist/chunks/index-vr82typq.js +23 -0
  11. package/dist/chunks/index-vr82typq.js.map +11 -0
  12. package/dist/chunks/index-zyg8amjc.js +23 -0
  13. package/dist/chunks/index-zyg8amjc.js.map +10 -0
  14. package/dist/errors/index.js +15 -30
  15. package/dist/errors/index.js.map +3 -4
  16. package/dist/fetcher/index.d.ts +5 -2
  17. package/dist/fetcher/index.d.ts.map +1 -1
  18. package/dist/fetcher/index.js +51 -26
  19. package/dist/fetcher/index.js.map +6 -5
  20. package/dist/fetcher/use-visible-interval.d.ts +24 -0
  21. package/dist/fetcher/use-visible-interval.d.ts.map +1 -0
  22. package/dist/fetcher/use-visible-revalidation.d.ts +10 -0
  23. package/dist/fetcher/use-visible-revalidation.d.ts.map +1 -0
  24. package/dist/oauth/answer.d.ts +24 -0
  25. package/dist/oauth/answer.d.ts.map +1 -0
  26. package/dist/oauth/consent.d.ts +88 -0
  27. package/dist/oauth/consent.d.ts.map +1 -0
  28. package/dist/oauth/index.d.ts +12 -0
  29. package/dist/oauth/index.d.ts.map +1 -0
  30. package/dist/oauth/index.js +106 -0
  31. package/dist/oauth/index.js.map +11 -0
  32. package/dist/oauth/model.d.ts +13 -0
  33. package/dist/oauth/model.d.ts.map +1 -0
  34. package/dist/oauth/react/index.d.ts +7 -0
  35. package/dist/oauth/react/index.d.ts.map +1 -0
  36. package/dist/oauth/react/index.js +87 -0
  37. package/dist/oauth/react/index.js.map +10 -0
  38. package/dist/oauth/react/use-consent.d.ts +47 -0
  39. package/dist/oauth/react/use-consent.d.ts.map +1 -0
  40. package/dist/session/index.js +30 -39
  41. package/dist/session/index.js.map +3 -5
  42. package/dist/session/react/index.js +31 -204
  43. package/dist/session/react/index.js.map +3 -8
  44. package/docs/README.md +3 -2
  45. package/docs/guide/fetcher.md +45 -3
  46. package/docs/guide/oauth.md +132 -0
  47. package/docs/roadmap.md +23 -1
  48. package/docs/troubleshooting.md +39 -0
  49. package/package.json +14 -2
package/README.md CHANGED
@@ -92,7 +92,7 @@ Vite's preview server, as `bun run start` would: the pages, `/api`, the
92
92
  client's files and the app's middlewares. So does React Router's prerendering.
93
93
 
94
94
  `@alxia/core` and `react-router` 8 are peers, and `vite` 7 or 8 is an
95
- optional peer, for `/vite`; `react` 19 is an optional peer, for `/pwa/react`, `/fetcher` and `/lists`. The six `workbox-*`
95
+ optional peer, for `/vite`; `react` 19 is an optional peer, for `/pwa/react`, `/fetcher`, `/lists` and `/oauth/react`. The six `workbox-*`
96
96
  packages (`workbox-core`, `workbox-expiration`, `workbox-navigation-preload`,
97
97
  `workbox-precaching`, `workbox-routing`, `workbox-strategies`) are optional
98
98
  peers too, for `/pwa/worker` (the [PWA section](#installable-and-offline-pwa) has the install line). `/errors` imports `react-router`, a required peer that is already installed. The package declares no dependency.
@@ -1175,6 +1175,25 @@ const { data, stale, state, retry } = useDebouncedFetcher<Found>(
1175
1175
  );
1176
1176
  ```
1177
1177
 
1178
+ A screen with no real-time channel reads its data again every N ms, **only while the page is visible**, and at once when a hidden page comes back after more than N ms:
1179
+
1180
+ ```tsx
1181
+ import { useFetcher } from 'react-router';
1182
+ import {
1183
+ useVisibleInterval,
1184
+ useVisibleRevalidation,
1185
+ } from '@alxia/react-router/fetcher';
1186
+
1187
+ // inside a component: the route's loaders, every minute
1188
+ useVisibleRevalidation(60_000);
1189
+
1190
+ // any callback, such as a fetcher's load; `enabled: false` pauses it
1191
+ const fetcher = useFetcher<Counts>();
1192
+ useVisibleInterval(() => void fetcher.load('/counts'), 300_000, {
1193
+ enabled: !menuOpen,
1194
+ });
1195
+ ```
1196
+
1178
1197
  See the [fetcher guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/fetcher.md).
1179
1198
 
1180
1199
  ## A list whose state is the URL
@@ -1241,6 +1260,57 @@ export function loader({ request }: Route.LoaderArgs) {
1241
1260
  `usePagedRows` reads `{ failed: true }` as a page without rows, so it sets `error` and keeps the rows. See [troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md). See the
1242
1261
  [lists guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/lists.md).
1243
1262
 
1263
+ ## The consent screen of an OAuth server
1264
+
1265
+ `@alxia/react-router/oauth` (the server side, `react-router` its only package import) and `@alxia/react-router/oauth/react`
1266
+ (the hook, `react` and `react-router`) hold the decision logic of the screen where a viewer allows or denies an OAuth
1267
+ client, over the GraphQL of `@nxgt/janus-oauth/graphql`. You pass the three calls as functions; the card and its words stay yours.
1268
+
1269
+ ```tsx
1270
+ // app/routes/oauth/consent.tsx
1271
+ import { alxiaOf } from '@alxia/react-router';
1272
+ import { consentShouldRevalidate, oauthConsent } from '@alxia/react-router/oauth';
1273
+ import { useConsent } from '@alxia/react-router/oauth/react';
1274
+ import type { Route } from './+types/consent';
1275
+
1276
+ const consent = oauthConsent({
1277
+ json, // your jsonActions()
1278
+ errors, // your createApiErrors(...)
1279
+ viewer: (request) => requireViewer(request), // throws a redirect to sign in, back to this address
1280
+ caller: (request) => sessionCaller(request),
1281
+ find: async (id, caller) => (await execute(OAuthRequestQuery, { id }, caller)).oauthRequest,
1282
+ approve: async ({ id, scopes }, caller) =>
1283
+ (await execute(ApproveOAuthRequestMutation, { id, scopes }, caller)).approveOAuthRequest,
1284
+ deny: async ({ id }, caller) => (await execute(DenyOAuthRequestMutation, { id }, caller)).denyOAuthRequest,
1285
+ });
1286
+
1287
+ export const loader = ({ request }: Route.LoaderArgs) => consent.loader(request); // { pending, viewer }, no-store
1288
+ export const action = ({ context, request }: Route.ActionArgs) => consent.action(request, alxiaOf(context).language);
1289
+ export const shouldRevalidate = consentShouldRevalidate;
1290
+
1291
+ export default function Consent({ loaderData }: Route.ComponentProps) {
1292
+ const { pending: request } = loaderData;
1293
+ if (!request) return <p>This request has expired.</p>;
1294
+ const { approve, deny, pending, expired, error, dialogProps } = useConsent(request.id, {
1295
+ request: stepUpRequest, // the step-up channels of useStepUpReplay
1296
+ confirm: stepUpConfirm,
1297
+ });
1298
+ return (
1299
+ <>
1300
+ <button type="button" disabled={Boolean(pending)} onClick={approve}>Allow</button>
1301
+ <button type="button" disabled={Boolean(pending)} onClick={deny}>Deny</button>
1302
+ {expired ? <p role="alert">This request has expired.</p> : null}
1303
+ {error ? <p role="alert">{error}</p> : null}
1304
+ <StepUpDialog {...dialogProps} />
1305
+ </>
1306
+ );
1307
+ }
1308
+ ```
1309
+
1310
+ A decision lands as a hard navigation to the client's callback (only `http:` and `https:` are followed); a session not
1311
+ confirmed lately opens the shared step-up and the decision is posted again by itself. See the
1312
+ [OAuth consent guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/oauth.md).
1313
+
1244
1314
  ## Traps
1245
1315
 
1246
1316
  - **A context key made in `app/` reaches the loaders only through the
@@ -1462,6 +1532,22 @@ From `@alxia/react-router/lists` (`react` and `react-router` are its only import
1462
1532
  | `usePagedRows(firstPage, rowsOf, { cursorParam?, nextOf? }?)`, `PagedRows`, `PagedRowsOptions` | `{ rows, next, loading, error, loadMore }`: appends the pages the same route answers to `?<cursorParam>=<next>` (default `after`, `nextOf` default `page.next`) through `useFetcher`; starts over when `firstPage` is another object; a failure keeps the rows and sets `error` (an answer that is not a page), and data that is not a page is ignored |
1463
1533
  | `orFailed(after, run)`, `FAILED`, `isFailedPage(value)`, `FailedPage` | the loader half of `usePagedRows`: `run()`'s answer; an error is rethrown when `after` is empty, and with an `after` it answers `FAILED` (`{ failed: true }`, typed `Promise<T \| FailedPage>`), except a 3xx `Response`, which is rethrown. `isFailedPage` narrows an answer |
1464
1534
 
1535
+ From `@alxia/react-router/oauth` (`react-router` is its only package import):
1536
+
1537
+ | export | |
1538
+ | --- | --- |
1539
+ | `oauthConsent({ json, errors, viewer, caller, find, approve, deny, notFound?, stepUpRequired? })`, `ConsentOptions`, `ConsentRoute`, `ConsentRedirect`, `ConsentInput` | `{ loader(request), action(request, language) }` for the consent route: the loader answers `{ pending, viewer }` with `Cache-Control: no-store` (a 404 with `pending: null` for an unknown request); the action approves or denies the posted JSON and answers a `ConsentAnswer` |
1540
+ | `consentShouldRevalidate(args)` | the route's `shouldRevalidate`: `false` after a submission, React Router's default otherwise |
1541
+ | `requestIdOf(url)` | the `request_id` (else `request`) of an authorization link, trimmed; `undefined` when empty |
1542
+ | `followableRedirect(value)` | the normalised address when `http:` or `https:`, else `undefined` |
1543
+ | `ConsentAnswer`, `ConsentDecisionIntent` | the types of the action's answer and of `'approve' \| 'deny'` |
1544
+
1545
+ From `@alxia/react-router/oauth/react` (`react` and `react-router` are its only imports):
1546
+
1547
+ | export | |
1548
+ | --- | --- |
1549
+ | `useConsent(requestId, { request, confirm, go?, action? })`, `UseConsentOptions` | `{ approve, deny, pending, expired, error, dialogProps }`: posts the decision as JSON, replays it after the shared step-up (`useStepUpReplay`), follows the callback with a hard navigation (`go`) and holds `pending` until the page is left |
1550
+
1465
1551
  From `@alxia/react-router/link` (`react` and `react-router` are its only imports; it renders on the server):
1466
1552
 
1467
1553
  | export | |
@@ -1475,6 +1561,8 @@ From `@alxia/react-router/fetcher` (`react` and `react-router` are its imports;
1475
1561
  | `useFetcherAnswer(fetcher, onAnswer?)`, `AnswerSource` | `onAnswer(answer)` once per answer that arrives while the fetcher is idle and differs from the last one seen (the data present at mount counts as seen); the handler is read from a ref; returns the latest answer handed over, `undefined` before the first |
1476
1562
  | `useFetcherPromise(fetcher, { refusal? }?)`, `FetcherPromiseOptions`, `FetcherPromiseAborted` | returns a stable `run(submit)` that calls `submit` (`() => fetcher.submit(...)`) and returns a Promise settled by the fetcher's next new answer: resolved with it, or rejected with an `Error` when `refusal(answer)` returns a message (or your own `Error`); an answer held before the call never settles it; a second `run` while one is pending rejects the older with `FetcherPromiseAborted` (`reason: 'superseded'`); an unmount rejects the pending one (`'unmounted'`); a `submit` that throws rejects with its error |
1477
1563
  | `useFormRefusal(result)`, `FormRefusalResult` | `useFormRefusal(fetcher.data)` returns the `FormErrors` of a fetcher answer that carries `fieldErrors` (`apiErrors.formRefusal`'s), `undefined` for no answer, a done step or a refusal without them; stable for one answer |
1564
+ | `useVisibleInterval(callback, ms, { enabled?, runOnVisible? }?)`, `VisibleIntervalOptions` | runs `callback` every `ms` only while `document.visibilityState` is `'visible'` (a tick while hidden is skipped); when a hidden page becomes visible after at least `ms` since the last run it runs at once (`runOnVisible`, default `true`); `enabled: false` stops it; the callback is read from a ref, so a new function never restarts the timer; the timer and listener are cleared on unmount; nothing starts on the server |
1565
+ | `useVisibleRevalidation(ms, { enabled? }?)` | `useVisibleInterval` over `useRevalidator`: revalidates the route's loaders every `ms` while the page is visible, skipping a tick while a revalidation is in flight |
1478
1566
  | `useDebouncedFetcher(value, to, { delayMs?, minLength?, enabled?, key?, keep? }?)`, `DebouncedFetcher`, `DebouncedFetcherOptions`, `DebouncedFetcherState` | a resource route loaded for what is typed: one `fetcher.load` after `delayMs` (250) without a change and from `minLength` (1) trimmed characters; `to` is a URL or `(value) => url`; returns `{ data, stale, state, retry }`: `data` is the answer for the value as it stands, else the previous one with `stale` set; `state` is `'idle' \| 'waiting' \| 'loading' \| 'done'`; `keep(answer)` filters what may stay as the stale answer; `retry()` (stable) asks again at once |
1479
1567
 
1480
1568
  From `@alxia/react-router/subscription` (no import, no peer):
@@ -1501,8 +1589,9 @@ The `alxia-react-router` bin, run with `bunx`:
1501
1589
  - [Installable and offline](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md): what the subpaths hold, the worker's contract, the worker, what it caches, the offline page, building the worker with `alxia()` or `buildWorker`, the hooks in React, registering the worker and offering an update, serving the worker's script, the web app manifest, and clearing at sign-out.
1502
1590
  - [An API's refusals](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/errors.md): what `/errors` holds, recognising an API error, the request id, the page a request is for, the catalogue store, the form checks in the API's words, the provider, wording a refusal, loaders and the error page, actions, JSON actions and CSRF, and keeping the catalogue fresh.
1503
1591
  - [A BFF session](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/session.md): redirects that stay on the site, the cookies, the sign-in route, the second factor, the viewer and sign-out, the step-up, and the pages a mail's link opens.
1504
- - [A fetcher's fresh answer](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/fetcher.md): `useFetcherAnswer`: a handler called once per new answer of a keyed fetcher, not for the data it held at mount; `useFetcherPromise`: one submission's answer as a Promise; `useDebouncedFetcher`: a resource route loaded for what is typed.
1592
+ - [A fetcher's fresh answer](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/fetcher.md): `useFetcherAnswer`: a handler called once per new answer of a keyed fetcher, not for the data it held at mount; `useFetcherPromise`: one submission's answer as a Promise; `useDebouncedFetcher`: a resource route loaded for what is typed; `useVisibleInterval` and `useVisibleRevalidation`: a callback or a revalidation on an interval while the page is visible.
1505
1593
  - [Lists whose state is the URL](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/lists.md): `useSearchChange`, `useSameRouteLoading`, `usePagedRows`: a search that drops the cursor, a skeleton for the same page reloading, a load-more button.
1594
+ - [The consent screen of an OAuth server](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/oauth.md): `oauthConsent`, a loader and an action over the janus-oauth GraphQL, and `useConsent`: the decision, the step-up replay and the callback.
1506
1595
  - [Subscriptions for a BFF](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/subscriptions.md): exposing a GraphQL subscription to the browser through a resource route: the allow-list, the session's bearer, the headers, the heartbeat, abort and rate limits.
1507
1596
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md): each message, and the traps that print none.
1508
1597
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/roadmap.md): what is coming, and what is not planned.
@@ -0,0 +1,186 @@
1
+ import {
2
+ INITIAL_STEP_UP2,
3
+ stepUpReducer2,
4
+ stepUpProps2
5
+ } from "./index-fpy7f4bz.js";
6
+
7
+ // src/session/react/step-up/answers.ts
8
+ var intentOf = (data) => typeof data === "object" && data !== null ? data.intent : undefined;
9
+ function requestAnswerOf2(data) {
10
+ return intentOf(data) === "request-step-up" ? data : undefined;
11
+ }
12
+ function confirmAnswerOf2(data) {
13
+ return intentOf(data) === "confirm-step-up" ? data : undefined;
14
+ }
15
+ // src/session/react/step-up/dialog-props.ts
16
+ function dialogPropsOf2(stepUp, open, cancel) {
17
+ return {
18
+ open,
19
+ phase: stepUp.phase,
20
+ via: stepUp.via,
21
+ props: stepUp.props,
22
+ message: stepUp.message,
23
+ busy: stepUp.busy,
24
+ confirming: stepUp.phase === "confirmed",
25
+ onCancel: cancel,
26
+ onRetry: () => stepUp.start(),
27
+ onSubmit: (code) => stepUp.submit(code),
28
+ onResend: stepUp.via === "email" ? () => stepUp.start() : undefined
29
+ };
30
+ }
31
+ // src/session/react/step-up/phase.ts
32
+ function stepUpDialogPhase2(state) {
33
+ if (state.phase === "done")
34
+ return "confirmed";
35
+ if (state.challenge)
36
+ return "code";
37
+ if (state.error === "expired")
38
+ return "expired";
39
+ return state.error ? "failed" : "asking";
40
+ }
41
+ function asksStepUp2(answer) {
42
+ if (typeof answer !== "object" || answer === null)
43
+ return false;
44
+ const a = answer;
45
+ return !a.ok && Boolean(a.stepUp);
46
+ }
47
+ // src/session/react/step-up/use-step-up.ts
48
+ import { useEffect, useReducer, useRef } from "react";
49
+ function useStepUp2({ request, confirm, onConfirmed }) {
50
+ const [state, dispatch] = useReducer(stepUpReducer2, INITIAL_STEP_UP2);
51
+ const confirmedRun = useRef(onConfirmed);
52
+ confirmedRun.current = onConfirmed;
53
+ const live = useRef(false);
54
+ const handledRequest = useRef(request.answer);
55
+ const handledConfirm = useRef(confirm.answer);
56
+ useEffect(() => {
57
+ const answer = request.answer;
58
+ if (!answer || handledRequest.current === answer)
59
+ return;
60
+ handledRequest.current = answer;
61
+ if (!live.current)
62
+ return;
63
+ dispatch({ type: "requested", answer });
64
+ }, [request.answer]);
65
+ useEffect(() => {
66
+ const answer = confirm.answer;
67
+ if (!answer || handledConfirm.current === answer)
68
+ return;
69
+ handledConfirm.current = answer;
70
+ if (!live.current)
71
+ return;
72
+ dispatch({ type: "confirmed", answer });
73
+ if (answer.ok)
74
+ confirmedRun.current();
75
+ }, [confirm.answer]);
76
+ return {
77
+ phase: stepUpDialogPhase2(state),
78
+ via: state.via,
79
+ props: stepUpProps2(state),
80
+ message: state.message,
81
+ busy: request.busy || confirm.busy,
82
+ start() {
83
+ live.current = true;
84
+ dispatch({ type: "start" });
85
+ request.post({});
86
+ },
87
+ submit(code) {
88
+ if (!state.challenge)
89
+ return;
90
+ dispatch({ type: "submit" });
91
+ confirm.post({ challenge: state.challenge, code, via: state.via });
92
+ },
93
+ release() {
94
+ live.current = false;
95
+ request.reset?.();
96
+ confirm.reset?.();
97
+ handledRequest.current = request.answer;
98
+ handledConfirm.current = confirm.answer;
99
+ dispatch({ type: "release" });
100
+ }
101
+ };
102
+ }
103
+ // src/session/react/step-up/use-step-up-replay.ts
104
+ import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
105
+ function useStepUpReplay2({
106
+ answer,
107
+ replay,
108
+ action,
109
+ spendsCode,
110
+ request,
111
+ confirm
112
+ }) {
113
+ const [open, setOpen] = useState(false);
114
+ const [confirmedNotice, setConfirmedNotice] = useState(false);
115
+ const isOpen = useRef2(false);
116
+ const replaying = useRef2(false);
117
+ const latest = useRef2({ replay, action, spendsCode });
118
+ latest.current = { replay, action, spendsCode };
119
+ function show(value) {
120
+ isOpen.current = value;
121
+ setOpen(value);
122
+ }
123
+ const noticeFor = useRef2(action);
124
+ useEffect2(() => {
125
+ if (noticeFor.current === action)
126
+ return;
127
+ noticeFor.current = action;
128
+ setConfirmedNotice(false);
129
+ }, [action]);
130
+ const stepUp = useStepUp2({
131
+ request,
132
+ confirm,
133
+ onConfirmed() {
134
+ if (!isOpen.current)
135
+ return;
136
+ const { replay, action, spendsCode } = latest.current;
137
+ const spent = stepUpRef.current.via === "secondFactor" && action !== undefined && spendsCode?.(action) === true;
138
+ if (spent) {
139
+ stepUpRef.current.release();
140
+ show(false);
141
+ setConfirmedNotice(true);
142
+ return;
143
+ }
144
+ replaying.current = true;
145
+ replay();
146
+ }
147
+ });
148
+ const stepUpRef = useRef2(stepUp);
149
+ stepUpRef.current = stepUp;
150
+ const handled = useRef2(answer);
151
+ useEffect2(() => {
152
+ if (!answer || handled.current === answer)
153
+ return;
154
+ handled.current = answer;
155
+ if (asksStepUp2(answer)) {
156
+ replaying.current = false;
157
+ setConfirmedNotice(false);
158
+ show(true);
159
+ stepUpRef.current.start();
160
+ return;
161
+ }
162
+ setConfirmedNotice(false);
163
+ if (replaying.current) {
164
+ replaying.current = false;
165
+ show(false);
166
+ stepUpRef.current.release();
167
+ }
168
+ }, [answer]);
169
+ function cancel() {
170
+ replaying.current = false;
171
+ stepUp.release();
172
+ show(false);
173
+ setConfirmedNotice(false);
174
+ }
175
+ return {
176
+ open,
177
+ confirmedNotice,
178
+ dialogProps: dialogPropsOf2(stepUp, open, cancel),
179
+ cancel,
180
+ stepUp
181
+ };
182
+ }
183
+ export { requestAnswerOf2, confirmAnswerOf2, dialogPropsOf2, stepUpDialogPhase2, asksStepUp2, useStepUp2, useStepUpReplay2 };
184
+
185
+ //# debugId=DE6A388AD1A5E27264756E2164756E21
186
+ //# sourceMappingURL=index-1f16k1sj.js.map
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/session/react/step-up/answers.ts", "../src/session/react/step-up/dialog-props.ts", "../src/session/react/step-up/phase.ts", "../src/session/react/step-up/use-step-up.ts", "../src/session/react/step-up/use-step-up-replay.ts"],
4
+ "sourcesContent": [
5
+ "import type {\n\tStepUpConfirmAnswer,\n\tStepUpRequestAnswer,\n} from '../../model/step-up-answers';\n\nconst intentOf = (data: unknown) =>\n\ttypeof data === 'object' && data !== null\n\t\t? (data as { intent?: unknown }).intent\n\t\t: undefined;\n\n/**\n * The step-up route answers both intents from one action, so a fetcher's data\n * is their union: this is the request's answer, or `undefined` for any other.\n */\nexport function requestAnswerOf(\n\tdata: unknown,\n): StepUpRequestAnswer | undefined {\n\treturn intentOf(data) === 'request-step-up'\n\t\t? (data as StepUpRequestAnswer)\n\t\t: undefined;\n}\n\n/** The confirm's answer out of the route's union, or `undefined` for any other. */\nexport function confirmAnswerOf(\n\tdata: unknown,\n): StepUpConfirmAnswer | undefined {\n\treturn intentOf(data) === 'confirm-step-up'\n\t\t? (data as StepUpConfirmAnswer)\n\t\t: undefined;\n}\n",
6
+ "import type { StepUpVia } from '../../model/second-factor';\nimport type { StepUpProps } from '../../model/step-up-props';\nimport type { StepUpDialogPhase } from './phase';\nimport type { useStepUp } from './use-step-up';\n\n/** What a step-up dialog is given: the phase to show and the handlers. */\nexport interface StepUpDialogProps {\n\topen: boolean;\n\tphase: StepUpDialogPhase;\n\t/** How the code is confirmed, in every phase once a challenge was granted (`undefined` before). */\n\tvia: StepUpVia | undefined;\n\t/** The words of the code form (`stepUpProps`). */\n\tprops: StepUpProps;\n\t/** The last refusal worded by the server. */\n\tmessage: string | undefined;\n\tbusy: boolean;\n\t/** The code was accepted and the action is being sent again. */\n\tconfirming: boolean;\n\tonCancel(): void;\n\t/** \"Retry\" after a refused request or an over challenge. */\n\tonRetry(): void;\n\tonSubmit(code: string): void;\n\t/** \"Resend code\", for a mailed code only. */\n\tonResend: (() => void) | undefined;\n}\n\n/** The dialog's props out of a step-up: plain, so any host can build them. */\nexport function dialogPropsOf(\n\tstepUp: ReturnType<typeof useStepUp>,\n\topen: boolean,\n\tcancel: () => void,\n): StepUpDialogProps {\n\treturn {\n\t\topen,\n\t\tphase: stepUp.phase,\n\t\tvia: stepUp.via,\n\t\tprops: stepUp.props,\n\t\tmessage: stepUp.message,\n\t\tbusy: stepUp.busy,\n\t\tconfirming: stepUp.phase === 'confirmed',\n\t\tonCancel: cancel,\n\t\tonRetry: () => stepUp.start(),\n\t\tonSubmit: (code) => stepUp.submit(code),\n\t\tonResend: stepUp.via === 'email' ? () => stepUp.start() : undefined,\n\t};\n}\n",
7
+ "import type { StepUpState } from '../../model/step-up';\n\n/** Where a step-up is, as a dialog shows it. */\nexport type StepUpDialogPhase =\n\t| 'asking'\n\t| 'failed'\n\t| 'code'\n\t| 'expired'\n\t| 'confirmed';\n\n/** The phase a reducer state is in: what the dialog shows. */\nexport function stepUpDialogPhase(state: StepUpState): StepUpDialogPhase {\n\tif (state.phase === 'done') return 'confirmed';\n\tif (state.challenge) return 'code';\n\tif (state.error === 'expired') return 'expired';\n\treturn state.error ? 'failed' : 'asking';\n}\n\n/** Whether an answer is a refusal asking for a step-up (`stepUp` set, not ok). */\nexport function asksStepUp(answer: unknown): boolean {\n\tif (typeof answer !== 'object' || answer === null) return false;\n\tconst a = answer as { ok?: unknown; stepUp?: unknown };\n\treturn !a.ok && Boolean(a.stepUp);\n}\n",
8
+ "import { useEffect, useReducer, useRef } from 'react';\nimport type { StepUpVia } from '../../model/second-factor';\nimport { INITIAL_STEP_UP, stepUpReducer } from '../../model/step-up';\nimport type {\n\tStepUpConfirmAnswer,\n\tStepUpRequestAnswer,\n} from '../../model/step-up-answers';\nimport { stepUpProps } from '../../model/step-up-props';\nimport { stepUpDialogPhase } from './phase';\n\n/**\n * One step-up intent as the host posts it: its own `post` (so the fields stay\n * typed by the host's action), the settled answer it got since it mounted\n * (`undefined` before), and whether it is in flight. `reset` forgets the\n * answer (a fetcher's `reset`).\n */\nexport interface StepUpChannel<Fields, Answer> {\n\tpost(fields: Fields): void;\n\treadonly answer: Answer | undefined;\n\treadonly busy: boolean;\n\treset?(): void;\n}\n\nexport interface StepUpOptions {\n\t/** Posts `{ intent: 'request-step-up' }`, to the route that serves `stepUpSteps().route`. */\n\trequest: StepUpChannel<Record<string, never>, StepUpRequestAnswer>;\n\t/** Posts `{ intent: 'confirm-step-up', challenge, code, via }`. */\n\tconfirm: StepUpChannel<\n\t\t{ challenge: string; code: string; via: StepUpVia | undefined },\n\t\tStepUpConfirmAnswer\n\t>;\n\t/** Runs once the API accepted the code. */\n\tonConfirmed(): void;\n}\n\n/**\n * The step-up as the page lives it, on `stepUpReducer`: `start` asks for a\n * challenge, `submit` posts the code, and once it is accepted `onConfirmed`\n * runs. The challenge lives in the reducer's state and the channels' answers\n * only; `release` forgets everything, and an answer that lands after it (a\n * request or a code in flight when the host closed) is ignored.\n */\nexport function useStepUp({ request, confirm, onConfirmed }: StepUpOptions) {\n\tconst [state, dispatch] = useReducer(stepUpReducer, INITIAL_STEP_UP);\n\tconst confirmedRun = useRef(onConfirmed);\n\tconfirmedRun.current = onConfirmed;\n\t// True from `start` to `release`: only then are answers this step-up's.\n\tconst live = useRef(false);\n\n\t// Each settled answer once, even one worded as the last (a new object).\n\tconst handledRequest = useRef(request.answer);\n\tconst handledConfirm = useRef(confirm.answer);\n\tuseEffect(() => {\n\t\tconst answer = request.answer;\n\t\tif (!answer || handledRequest.current === answer) return;\n\t\thandledRequest.current = answer;\n\t\tif (!live.current) return;\n\t\tdispatch({ type: 'requested', answer });\n\t}, [request.answer]);\n\tuseEffect(() => {\n\t\tconst answer = confirm.answer;\n\t\tif (!answer || handledConfirm.current === answer) return;\n\t\thandledConfirm.current = answer;\n\t\tif (!live.current) return;\n\t\tdispatch({ type: 'confirmed', answer });\n\t\tif (answer.ok) confirmedRun.current();\n\t}, [confirm.answer]);\n\n\treturn {\n\t\tphase: stepUpDialogPhase(state),\n\t\t/** How the challenge held (or the last one) is confirmed. */\n\t\tvia: state.via,\n\t\t/** What a dialog words its texts from (`stepUpProps`). */\n\t\tprops: stepUpProps(state),\n\t\t/** The last refusal worded by the server: the fallback of the dialog's own words. */\n\t\tmessage: state.message,\n\t\t/** A request or a code is in flight. */\n\t\tbusy: request.busy || confirm.busy,\n\t\t/** Asks for a challenge: the first, or \"Resend code\" / \"New code\". */\n\t\tstart() {\n\t\t\tlive.current = true;\n\t\t\tdispatch({ type: 'start' });\n\t\t\trequest.post({});\n\t\t},\n\t\t/** Posts the code for the challenge held. */\n\t\tsubmit(code: string) {\n\t\t\tif (!state.challenge) return;\n\t\t\tdispatch({ type: 'submit' });\n\t\t\tconfirm.post({ challenge: state.challenge, code, via: state.via });\n\t\t},\n\t\t/** Forgets the challenge, the answers and the errors. */\n\t\trelease() {\n\t\t\tlive.current = false;\n\t\t\trequest.reset?.();\n\t\t\tconfirm.reset?.();\n\t\t\thandledRequest.current = request.answer;\n\t\t\thandledConfirm.current = confirm.answer;\n\t\t\tdispatch({ type: 'release' });\n\t\t},\n\t};\n}\n",
9
+ "import { useEffect, useRef, useState } from 'react';\nimport { dialogPropsOf } from './dialog-props';\nimport { asksStepUp } from './phase';\nimport { type StepUpOptions, useStepUp } from './use-step-up';\n\nexport interface StepUpReplayOptions<Action>\n\textends Pick<StepUpOptions, 'request' | 'confirm'> {\n\t/**\n\t * The settled answer of the sensitive action. A refusal carrying `stepUp`\n\t * opens the step-up; the first answer after the replay closes it.\n\t */\n\tanswer: unknown;\n\t/** Sends the refused action again, once the session is confirmed. */\n\treplay(): void;\n\t/** The action that was refused, for `spendsCode`. */\n\taction?: Action | undefined;\n\t/**\n\t * Whether the action carries a one-time code that a step-up by the\n\t * second factor just spent. Such an action is not replayed: the dialog\n\t * closes and `confirmedNotice` is true (\"confirmed, type a new code\").\n\t * The notice clears when `action` becomes another value (a new action was\n\t * posted), at once and not at its answer, so pass the action as you post\n\t * it, a new object for each post.\n\t */\n\tspendsCode?(action: Action): boolean;\n}\n\n/**\n * The step-up a sensitive action asks for: an answer carrying `stepUp` opens\n * it; once the session is confirmed the same action is sent again by itself\n * (`replay`), except an action that `spendsCode` after a step-up by the\n * second factor, whose code was just spent: the user types the next one\n * (`confirmedNotice`). The dialog stays, saying so, until the replayed\n * action is answered; if that is refused for a step-up again, it asks again.\n * If `replay` posts nothing, no answer ends that wait: call `cancel()`.\n * Generic over the host's `post`s, so each intent keeps its typed fields.\n *\n * @example\n * const { open, dialogProps, cancel, confirmedNotice } = useStepUpReplay({\n * \tanswer: fetcher.data,\n * \treplay: () => post(lastAction.current),\n * \trequest: { post: () => stepUpPost({ intent: 'request-step-up' }), answer, busy },\n * \tconfirm: { post: (f) => stepUpPost({ intent: 'confirm-step-up', ...f }), answer, busy },\n * });\n */\nexport function useStepUpReplay<Action = never>({\n\tanswer,\n\treplay,\n\taction,\n\tspendsCode,\n\trequest,\n\tconfirm,\n}: StepUpReplayOptions<Action>) {\n\tconst [open, setOpen] = useState(false);\n\tconst [confirmedNotice, setConfirmedNotice] = useState(false);\n\tconst isOpen = useRef(false);\n\tconst replaying = useRef(false);\n\tconst latest = useRef({ replay, action, spendsCode });\n\tlatest.current = { replay, action, spendsCode };\n\n\tfunction show(value: boolean) {\n\t\tisOpen.current = value;\n\t\tsetOpen(value);\n\t}\n\n\t// The notice belongs to the action it was shown for: another action\n\t// posted clears it at once, not at that action's answer.\n\tconst noticeFor = useRef(action);\n\tuseEffect(() => {\n\t\tif (noticeFor.current === action) return;\n\t\tnoticeFor.current = action;\n\t\tsetConfirmedNotice(false);\n\t}, [action]);\n\n\tconst stepUp = useStepUp({\n\t\trequest,\n\t\tconfirm,\n\t\tonConfirmed() {\n\t\t\t// A code accepted after the host cancelled replays nothing.\n\t\t\tif (!isOpen.current) return;\n\t\t\tconst { replay, action, spendsCode } = latest.current;\n\t\t\tconst spent =\n\t\t\t\tstepUpRef.current.via === 'secondFactor' &&\n\t\t\t\taction !== undefined &&\n\t\t\t\tspendsCode?.(action) === true;\n\t\t\tif (spent) {\n\t\t\t\tstepUpRef.current.release();\n\t\t\t\tshow(false);\n\t\t\t\tsetConfirmedNotice(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// The dialog stays, saying so, until the action is answered.\n\t\t\treplaying.current = true;\n\t\t\treplay();\n\t\t},\n\t});\n\tconst stepUpRef = useRef(stepUp);\n\tstepUpRef.current = stepUp;\n\n\t// Each answer once: a refusal asking for a step-up opens it; the answer\n\t// of the replayed action closes it (a new step-up opens it again).\n\tconst handled = useRef(answer);\n\tuseEffect(() => {\n\t\tif (!answer || handled.current === answer) return;\n\t\thandled.current = answer;\n\t\tif (asksStepUp(answer)) {\n\t\t\treplaying.current = false;\n\t\t\tsetConfirmedNotice(false);\n\t\t\tshow(true);\n\t\t\tstepUpRef.current.start();\n\t\t\treturn;\n\t\t}\n\t\tsetConfirmedNotice(false);\n\t\tif (replaying.current) {\n\t\t\treplaying.current = false;\n\t\t\tshow(false);\n\t\t\tstepUpRef.current.release();\n\t\t}\n\t}, [answer]);\n\n\tfunction cancel() {\n\t\treplaying.current = false;\n\t\tstepUp.release();\n\t\tshow(false);\n\t\tsetConfirmedNotice(false);\n\t}\n\n\treturn {\n\t\topen,\n\t\t/** True after a step-up by the second factor spent the action's own code. */\n\t\tconfirmedNotice,\n\t\tdialogProps: dialogPropsOf(stepUp, open, cancel),\n\t\t/** Closes the dialog without sending the action again. */\n\t\tcancel,\n\t\tstepUp,\n\t};\n}\n"
10
+ ],
11
+ "mappings": "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",
12
+ "debugId": "DE6A388AD1A5E27264756E2164756E21",
13
+ "names": []
14
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ isCrossSite
3
+ } from "./index-mscfme4r.js";
4
+
5
+ // src/errors/page-path.ts
6
+ function nextOf2(url) {
7
+ const { pathname, searchParams } = new URL(url);
8
+ const trimmed = pathname.endsWith("/_.data") ? pathname.slice(0, -"_.data".length) : pathname.replace(/\.data$/, "");
9
+ const path = trimmed.replace(/^\/{2,}/, "/");
10
+ const params = new URLSearchParams(searchParams);
11
+ params.delete("_routes");
12
+ for (const value of params.getAll("index")) {
13
+ if (!value)
14
+ params.delete("index", value);
15
+ }
16
+ const search = params.toString();
17
+ return search ? `${path}?${search}` : path;
18
+ }
19
+
20
+ // src/errors/same-site.ts
21
+ import { data } from "react-router";
22
+ function siteCheck(origin) {
23
+ return (request) => {
24
+ const set = typeof origin === "function" ? origin() : origin;
25
+ if (isCrossSite(request, set)) {
26
+ throw data("Cross-site request refused", { status: 403 });
27
+ }
28
+ };
29
+ }
30
+ var SITE_CHECK = Symbol.for("@alxia/react-router:site-check@1");
31
+ function rememberSiteCheck(actions, check) {
32
+ Object.defineProperty(actions, SITE_CHECK, {
33
+ value: check,
34
+ enumerable: true,
35
+ writable: false
36
+ });
37
+ }
38
+ var hasSiteCheck = (actions) => typeof actions[SITE_CHECK] === "function";
39
+ function refuseCrossSite(actions, request) {
40
+ const check = actions[SITE_CHECK];
41
+ (typeof check === "function" ? check : siteCheck())(request);
42
+ }
43
+
44
+ export { nextOf2, siteCheck, rememberSiteCheck, hasSiteCheck, refuseCrossSite };
45
+
46
+ //# debugId=E25AC4C91AE67D1C64756E2164756E21
47
+ //# sourceMappingURL=index-atxcmy0f.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors/page-path.ts", "../src/errors/same-site.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * The page a request is for, as `next` carries it back after sign-in: its\n * path and search, without React Router's single-fetch details. A client\n * navigation's `request.url` is `/todos/demo/list.data?q=a&_routes=…`\n * (`/_.data` for `/`, `?index` for an index action), as the server\n * runtime's own `normalizeUrl` strips them.\n */\nexport function nextOf(url: string): string {\n\tconst { pathname, searchParams } = new URL(url);\n\tconst trimmed = pathname.endsWith('/_.data')\n\t\t? pathname.slice(0, -'_.data'.length)\n\t\t: pathname.replace(/\\.data$/, '');\n\t// `new URL('http://h/\\\\evil.com')` has the path `//evil.com`: never let a\n\t// `next` start with two slashes, which a redirect reads as another site.\n\tconst path = trimmed.replace(/^\\/{2,}/, '/');\n\tconst params = new URLSearchParams(searchParams);\n\tparams.delete('_routes');\n\t// An empty `index` names the index route; a valued one is the page's own.\n\tfor (const value of params.getAll('index')) {\n\t\tif (!value) params.delete('index', value);\n\t}\n\tconst search = params.toString();\n\treturn search ? `${path}?${search}` : path;\n}\n",
6
+ "import { data } from 'react-router';\nimport { isCrossSite } from './cross-site';\n\n/** Refuses a request from another site with a 403; reads headers only. */\nexport type SiteCheck = (request: Request) => void;\n\n/**\n * The same-site rule of `readJson`: an `Origin` that is not the app's own, or\n * a `Sec-Fetch-Site` that is neither `same-origin` nor `none`, is a 403.\n * `origin` is the app's configured origin (or a getter), else the request's.\n */\nexport function siteCheck(\n\torigin?: string | (() => string | undefined) | undefined,\n): SiteCheck {\n\treturn (request) => {\n\t\tconst set = typeof origin === 'function' ? origin() : origin;\n\t\tif (isCrossSite(request, set)) {\n\t\t\tthrow data('Cross-site request refused', { status: 403 });\n\t\t}\n\t};\n}\n\n/**\n * Where a `jsonActions` instance keeps its site check: an enumerable property\n * under a registered symbol, so it survives `{ ...json }` and a second copy of\n * the package. Not public.\n */\nexport const SITE_CHECK = Symbol.for('@alxia/react-router:site-check@1');\n\n/** Puts the check on a `jsonActions` instance. */\nexport function rememberSiteCheck(actions: object, check: SiteCheck): void {\n\tObject.defineProperty(actions, SITE_CHECK, {\n\t\tvalue: check,\n\t\tenumerable: true,\n\t\twritable: false,\n\t});\n}\n\n/** Whether `actions` carries a site check, as every `jsonActions()` instance does. */\nexport const hasSiteCheck = (actions: object): boolean =>\n\ttypeof (actions as Record<symbol, unknown>)[SITE_CHECK] === 'function';\n\n/**\n * Refuses a cross-site request with the rule of the `jsonActions` instance\n * (its configured origin, read at each request); an object without a check\n * gets the request's own origin. It reads headers only, never the body.\n */\nexport function refuseCrossSite(actions: object, request: Request): void {\n\tconst check = (actions as Record<symbol, unknown>)[SITE_CHECK];\n\t(typeof check === 'function' ? (check as SiteCheck) : siteCheck())(request);\n}\n"
7
+ ],
8
+ "mappings": ";;;;;AAOO,SAAS,OAAM,CAAC,KAAqB;AAAA,EAC3C,QAAQ,UAAU,iBAAiB,IAAI,IAAI,GAAG;AAAA,EAC9C,MAAM,UAAU,SAAS,SAAS,SAAS,IACxC,SAAS,MAAM,GAAG,CAAC,SAAS,MAAM,IAClC,SAAS,QAAQ,WAAW,EAAE;AAAA,EAGjC,MAAM,OAAO,QAAQ,QAAQ,WAAW,GAAG;AAAA,EAC3C,MAAM,SAAS,IAAI,gBAAgB,YAAY;AAAA,EAC/C,OAAO,OAAO,SAAS;AAAA,EAEvB,WAAW,SAAS,OAAO,OAAO,OAAO,GAAG;AAAA,IAC3C,IAAI,CAAC;AAAA,MAAO,OAAO,OAAO,SAAS,KAAK;AAAA,EACzC;AAAA,EACA,MAAM,SAAS,OAAO,SAAS;AAAA,EAC/B,OAAO,SAAS,GAAG,QAAQ,WAAW;AAAA;;;ACtBvC;AAWO,SAAS,SAAS,CACxB,QACY;AAAA,EACZ,OAAO,CAAC,YAAY;AAAA,IACnB,MAAM,MAAM,OAAO,WAAW,aAAa,OAAO,IAAI;AAAA,IACtD,IAAI,YAAY,SAAS,GAAG,GAAG;AAAA,MAC9B,MAAM,KAAK,8BAA8B,EAAE,QAAQ,IAAI,CAAC;AAAA,IACzD;AAAA;AAAA;AASK,IAAM,aAAa,OAAO,IAAI,kCAAkC;AAGhE,SAAS,iBAAiB,CAAC,SAAiB,OAAwB;AAAA,EAC1E,OAAO,eAAe,SAAS,YAAY;AAAA,IAC1C,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,UAAU;AAAA,EACX,CAAC;AAAA;AAIK,IAAM,eAAe,CAAC,YAC5B,OAAQ,QAAoC,gBAAgB;AAOtD,SAAS,eAAe,CAAC,SAAiB,SAAwB;AAAA,EACxE,MAAM,QAAS,QAAoC;AAAA,GAClD,OAAO,UAAU,aAAc,QAAsB,UAAU,GAAG,OAAO;AAAA;",
9
+ "debugId": "E25AC4C91AE67D1C64756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,30 @@
1
+ import {
2
+ isApiErrorLike2,
3
+ routeError2
4
+ } from "./index-hn6htqc2.js";
5
+
6
+ // src/errors/fetcher-result.ts
7
+ import { data } from "react-router";
8
+ function fetcherRefusal(errors, error, language, route) {
9
+ const refusal = isApiErrorLike2(error) && error.status === 401 ? undefined : errors.apiRefusal(error, language);
10
+ if (!refusal)
11
+ throw routeError2(error, route);
12
+ return refusal;
13
+ }
14
+ async function fetcherResult(errors, language, about, run, options) {
15
+ try {
16
+ return { ok: true, ...about, ...await run() };
17
+ } catch (error) {
18
+ const { message, status } = fetcherRefusal(errors, error, language, options);
19
+ return data({
20
+ ok: false,
21
+ ...about,
22
+ error: (isApiErrorLike2(error) ? options?.reword?.(error) : undefined) ?? message
23
+ }, { status });
24
+ }
25
+ }
26
+
27
+ export { fetcherRefusal, fetcherResult };
28
+
29
+ //# debugId=D74C6D167773F20264756E2164756E21
30
+ //# sourceMappingURL=index-gchzf3mc.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors/fetcher-result.ts"],
4
+ "sourcesContent": [
5
+ "import { data } from 'react-router';\nimport { type ApiErrorLike, isApiErrorLike } from './api-error';\nimport type { ApiErrors, ApiRefusal } from './api-errors';\nimport { type RouteErrorOptions, routeError } from './route-error';\n\n/**\n * A fetcher's refusal: the API's (`apiRefusal`), an unreachable API's too,\n * so the page stays where it was. A 401 (the session caller redirects\n * first) or any other failure is thrown, for the error page.\n */\nexport function fetcherRefusal<L extends string>(\n\terrors: Pick<ApiErrors<L>, 'apiRefusal'>,\n\terror: unknown,\n\tlanguage: L,\n\troute?: RouteErrorOptions,\n): ApiRefusal {\n\tconst refusal =\n\t\tisApiErrorLike(error) && error.status === 401\n\t\t\t? undefined\n\t\t\t: errors.apiRefusal(error, language);\n\tif (!refusal) throw routeError(error, route);\n\treturn refusal;\n}\n\n/** What `fetcherResult` takes after `run`: `routeError`'s options, and a rewording. */\nexport interface FetcherResultOptions extends RouteErrorOptions {\n\t/**\n\t * Words the refusal of this call: given the caught API error, the message\n\t * to answer, or `undefined` to keep the worded one. The status is the\n\t * refusal's. Called for an API's own refusal only: not for an API it\n\t * cannot reach or any other failure, nor for what is thrown to the error\n\t * page.\n\t */\n\treword?: (error: ApiErrorLike) => string | undefined;\n}\n\n/**\n * Runs the mutation of an action a fetcher posted to: `{ ok: true }` with\n * `about` and the mutation's value, or the refusal (`fetcherRefusal`) as a\n * worded `error`, with its HTTP status, for a toast or a form message.\n */\nexport async function fetcherResult<\n\tL extends string,\n\tAbout extends object,\n\tValue extends object = Record<never, never>,\n>(\n\terrors: Pick<ApiErrors<L>, 'apiRefusal'>,\n\tlanguage: L,\n\tabout: About,\n\trun: () => Promise<Value>,\n\toptions?: FetcherResultOptions,\n) {\n\ttry {\n\t\treturn { ok: true as const, ...about, ...(await run()) };\n\t} catch (error) {\n\t\tconst { message, status } = fetcherRefusal(\n\t\t\terrors,\n\t\t\terror,\n\t\t\tlanguage,\n\t\t\toptions,\n\t\t);\n\t\treturn data(\n\t\t\t{\n\t\t\t\tok: false as const,\n\t\t\t\t...about,\n\t\t\t\terror:\n\t\t\t\t\t(isApiErrorLike(error) ? options?.reword?.(error) : undefined) ??\n\t\t\t\t\tmessage,\n\t\t\t},\n\t\t\t{ status },\n\t\t);\n\t}\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;AAAA;AAUO,SAAS,cAAgC,CAC/C,QACA,OACA,UACA,OACa;AAAA,EACb,MAAM,UACL,gBAAe,KAAK,KAAK,MAAM,WAAW,MACvC,YACA,OAAO,WAAW,OAAO,QAAQ;AAAA,EACrC,IAAI,CAAC;AAAA,IAAS,MAAM,YAAW,OAAO,KAAK;AAAA,EAC3C,OAAO;AAAA;AAoBD,eAAe,aAIrB,CACA,QACA,UACA,OACA,KACA,SACC;AAAA,EACD,IAAI;AAAA,IACH,OAAO,EAAE,IAAI,SAAkB,UAAW,MAAM,IAAI,EAAG;AAAA,IACtD,OAAO,OAAO;AAAA,IACf,QAAQ,SAAS,WAAW,eAC3B,QACA,OACA,UACA,OACD;AAAA,IACA,OAAO,KACN;AAAA,MACC,IAAI;AAAA,SACD;AAAA,MACH,QACE,gBAAe,KAAK,IAAI,SAAS,SAAS,KAAK,IAAI,cACpD;AAAA,IACF,GACA,EAAE,OAAO,CACV;AAAA;AAAA;",
8
+ "debugId": "D74C6D167773F20264756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,7 +1,3 @@
1
- import {
2
- isCrossSite
3
- } from "./index-mscfme4r.js";
4
-
5
1
  // src/errors/api-error.ts
6
2
  var isNamed = (error, name) => error instanceof Error && error.name === name;
7
3
  function isApiErrorLike2(error) {
@@ -118,46 +114,7 @@ function errorRequestId2(error) {
118
114
  return typeof requestId === "string" && requestId ? requestId : undefined;
119
115
  }
120
116
 
121
- // src/errors/page-path.ts
122
- function nextOf2(url) {
123
- const { pathname, searchParams } = new URL(url);
124
- const trimmed = pathname.endsWith("/_.data") ? pathname.slice(0, -"_.data".length) : pathname.replace(/\.data$/, "");
125
- const path = trimmed.replace(/^\/{2,}/, "/");
126
- const params = new URLSearchParams(searchParams);
127
- params.delete("_routes");
128
- for (const value of params.getAll("index")) {
129
- if (!value)
130
- params.delete("index", value);
131
- }
132
- const search = params.toString();
133
- return search ? `${path}?${search}` : path;
134
- }
135
-
136
- // src/errors/same-site.ts
137
- import { data as data2 } from "react-router";
138
- function siteCheck(origin) {
139
- return (request) => {
140
- const set = typeof origin === "function" ? origin() : origin;
141
- if (isCrossSite(request, set)) {
142
- throw data2("Cross-site request refused", { status: 403 });
143
- }
144
- };
145
- }
146
- var SITE_CHECK = Symbol.for("@alxia/react-router:site-check@1");
147
- function rememberSiteCheck(actions, check) {
148
- Object.defineProperty(actions, SITE_CHECK, {
149
- value: check,
150
- enumerable: true,
151
- writable: false
152
- });
153
- }
154
- var hasSiteCheck = (actions) => typeof actions[SITE_CHECK] === "function";
155
- function refuseCrossSite(actions, request) {
156
- const check = actions[SITE_CHECK];
157
- (typeof check === "function" ? check : siteCheck())(request);
158
- }
159
-
160
- export { isApiErrorLike2, waitMinutes2, retryAfterOf2, attemptsLeftOf2, valuesOf, isStatusError, isUnavailableError, ProblemError2, problemError2, isProblemErrorLike2, REQUEST_ID_HEADER2, requestIdOf2, routeError2, errorRequestId2, nextOf2, siteCheck, rememberSiteCheck, hasSiteCheck, refuseCrossSite };
117
+ export { isApiErrorLike2, waitMinutes2, retryAfterOf2, attemptsLeftOf2, valuesOf, isStatusError, isUnavailableError, ProblemError2, problemError2, isProblemErrorLike2, REQUEST_ID_HEADER2, requestIdOf2, routeError2, errorRequestId2 };
161
118
 
162
- //# debugId=A88B070738C15B2B64756E2164756E21
163
- //# sourceMappingURL=index-2swe47m6.js.map
119
+ //# debugId=CDA3CA2B382E0AF864756E2164756E21
120
+ //# sourceMappingURL=index-hn6htqc2.js.map
@@ -1,15 +1,13 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/errors/api-error.ts", "../src/errors/route-error.ts", "../src/errors/problem-error.ts", "../src/errors/request-id.ts", "../src/errors/page-path.ts", "../src/errors/same-site.ts"],
3
+ "sources": ["../src/errors/api-error.ts", "../src/errors/route-error.ts", "../src/errors/problem-error.ts", "../src/errors/request-id.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * The shape of an API's refusal, as `@nxgt/graphql-client`'s `ApiError`\n * carries it, declared structurally: `/errors` never imports the client, so\n * an app needs it only if it calls the API through it. `api-error.spec.ts`\n * holds the two in step.\n */\n\n/** One refused input field: `extensions.fields` of a validation refusal. */\nexport interface ApiFieldErrorLike {\n\treadonly path: string;\n\treadonly code: string;\n\treadonly message: string;\n\t/** The values the field's message interpolates. */\n\treadonly params?: Readonly<Record<string, unknown>>;\n}\n\n/** The values a message interpolates; a throttle's wait is `minutes` or `retryAfter`. */\nexport interface ApiErrorParamsLike {\n\treadonly minutes?: unknown;\n\treadonly retryAfter?: unknown;\n\treadonly attemptsLeft?: unknown;\n\treadonly [key: string]: unknown;\n}\n\n/**\n * One GraphQL error's `extensions`. `code` is the API's own\n * `<module>.errors.<slug>`; the rest is what its messages name.\n */\nexport interface ApiErrorExtensionsLike {\n\treadonly code?: string;\n\t/** The values the message interpolates, when the API sends them. */\n\treadonly params?: ApiErrorParamsLike;\n\t/** A validation refusal: every field at fault, by input path. */\n\treadonly fields?: readonly ApiFieldErrorLike[];\n\t/** Any other refusal about one input field, by its path. */\n\treadonly field?: string;\n\t/** A throttle's wait, in minutes (`waitMinutes` coerces a string). */\n\treadonly minutes?: unknown;\n\t/** A throttle's wait, in seconds. */\n\treadonly retryAfter?: unknown;\n\t/** The tries a refused code leaves, as `@nxgt/janus` counts them. */\n\treadonly attemptsLeft?: unknown;\n\treadonly [key: string]: unknown;\n}\n\n/** One error of a response's `errors`. */\nexport interface ApiErrorEntryLike {\n\treadonly message: string;\n\treadonly extensions?: ApiErrorExtensionsLike;\n}\n\n/** A refusal read from a GraphQL API: what `ApiError` has, and the request id. */\nexport interface ApiErrorLike extends Error {\n\treadonly errors: readonly ApiErrorEntryLike[];\n\t/** The first error's `extensions.code`. */\n\treadonly code: string | undefined;\n\t/** The HTTP status to answer with. */\n\treadonly status: number;\n\t/** The id the API logged the request under, when the caller kept it. */\n\treadonly requestId?: string;\n}\n\n/** Whether `error` is an `Error` of this `name`: the client's errors are told by it, never `instanceof`. */\nexport const isNamed = (error: unknown, name: string): error is Error =>\n\terror instanceof Error && error.name === name;\n\n/**\n * Whether `error` is an `ApiError` of `@nxgt/graphql-client`, by the test\n * that client's `isApiError` makes (an `Error` named `ApiError` with an\n * `errors` array), plus a numeric `status`, which `ApiErrorLike` promises.\n * It holds across two copies of the client.\n */\nexport function isApiErrorLike(error: unknown): error is ApiErrorLike {\n\treturn (\n\t\tisNamed(error, 'ApiError') &&\n\t\tArray.isArray((error as { errors?: unknown }).errors) &&\n\t\ttypeof (error as { status?: unknown }).status === 'number'\n\t);\n}\n\n/**\n * The minutes a wait lasts, as the API words it: `minutes` (at least one, in\n * `extensions.params` or beside it), else the `retryAfter` seconds rounded\n * up, else one.\n */\nexport function waitMinutes(\n\textensions: ApiErrorExtensionsLike | undefined,\n): number {\n\tconst minutes = extensions?.params?.minutes ?? extensions?.minutes;\n\tif (minutes !== undefined) return Math.max(1, Number(minutes) || 1);\n\tconst seconds = extensions?.params?.retryAfter ?? extensions?.retryAfter;\n\treturn Math.max(1, Math.ceil(Number(seconds) / 60) || 1);\n}\n\nconst MAX_RETRY_AFTER = 86_400;\n\n/** A wait in seconds from one value: a finite number from 0, or a decimal string. */\nfunction secondsOf(value: unknown): number | undefined {\n\tconst seconds =\n\t\ttypeof value === 'string' && /^\\d+(\\.\\d+)?$/.test(value.trim())\n\t\t\t? Number(value.trim())\n\t\t\t: value;\n\treturn typeof seconds === 'number' && Number.isFinite(seconds) && seconds >= 0\n\t\t? Math.min(MAX_RETRY_AFTER, Math.ceil(seconds)) + 0\n\t\t: undefined;\n}\n\n/**\n * The seconds a throttle asks to wait: `retryAfter` of the first error's\n * `extensions.params`, else (when that is unusable) of its `extensions`.\n * Only a finite number from 0, or a decimal string, counts; it is rounded up\n * and held at a day (86 400), and `-0` is 0. `undefined` when the API sent\n * none usable. Exported from `/errors`; `/session` words it as a typed\n * `retryAfter`.\n */\nexport function retryAfterOf(error: ApiErrorLike): number | undefined {\n\tconst extensions = error.errors[0]?.extensions;\n\treturn (\n\t\tsecondsOf(extensions?.params?.retryAfter) ??\n\t\tsecondsOf(extensions?.retryAfter)\n\t);\n}\n\n/**\n * The tries a refused code leaves, as `@nxgt/janus` counts them:\n * `extensions.attemptsLeft` of the first error, else the same key in its\n * `params`. A whole number from 0 up; `undefined` when the API sent none or\n * something that is not a count.\n */\nexport function attemptsLeftOf(error: ApiErrorLike): number | undefined {\n\tconst extensions = error.errors[0]?.extensions;\n\tfor (const value of [\n\t\textensions?.attemptsLeft,\n\t\textensions?.params?.attemptsLeft,\n\t]) {\n\t\tif (typeof value === 'number' && Number.isInteger(value) && value >= 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The values a message names: `extensions.params` over the extensions\n * themselves, where older errors and the extras of a few sit.\n */\nexport function valuesOf(\n\textensions: ApiErrorExtensionsLike | undefined,\n): Record<string, unknown> | undefined {\n\tif (!extensions) return undefined;\n\treturn extensions.params\n\t\t? { ...extensions, ...extensions.params }\n\t\t: extensions;\n}\n\n/** `@nxgt/graphql-client`'s `ApiStatusError`, as far as the failure reads it. */\nexport interface StatusErrorLike extends Error {\n\tstatus: number;\n\tstatusText?: string | undefined;\n\tbody?: unknown;\n}\n\n/** `@nxgt/graphql-client`'s `ApiUnavailableError`, as far as the failure reads it. */\nexport interface UnavailableErrorLike extends Error {\n\treason: string;\n}\n\nexport const isStatusError = (error: unknown): error is StatusErrorLike =>\n\tisNamed(error, 'ApiStatusError') &&\n\ttypeof (error as { status?: unknown }).status === 'number';\n\nexport const isUnavailableError = (\n\terror: unknown,\n): error is UnavailableErrorLike =>\n\tisNamed(error, 'ApiUnavailableError') &&\n\ttypeof (error as { reason?: unknown }).reason === 'string';\n",
6
6
  "import { data } from 'react-router';\nimport { isApiErrorLike } from './api-error';\nimport { isProblemErrorLike } from './problem-error';\nimport { requestIdOf } from './request-id';\n\n/** The statuses an error page words: unauthenticated, forbidden, missing, unavailable. */\nconst ROUTED: ReadonlySet<number> = new Set([401, 403, 404, 503]);\n\n/**\n * What a route error of the API carries to the error page: the API's code\n * and message when it gave a code, and the `X-Request-Id` of the call as the\n * support reference (`errorRequestId`).\n */\nexport type RouteErrorData = {\n\tcode?: string | undefined;\n\tmessage?: string | undefined;\n\trequestId?: string | undefined;\n};\n\nexport interface RouteErrorOptions {\n\t/** The statuses that become a route error of their own; 401, 403, 404 and 503 by default. */\n\tstatuses?: ReadonlySet<number>;\n}\n\n/**\n * What a loader throws for an error from the API: an `ApiError` whose\n * status is routed (401, 403, 404 or 503) becomes a route error response of\n * that status, which the root `ErrorBoundary` renders as its page, with the\n * call's request id when the API gave one. A thrown routed `Response` that\n * carries a request id becomes one too (a bare `Response` hands the browser\n * its body only). Any other `ApiError` that has a request id becomes a 500\n * with just that id, so the generic error page can show it. A `ProblemError`\n * (`problemError`, a REST API's RFC 9457 reply) is treated alike, its message\n * being the problem's detail or title. Anything else\n * is returned as it is.\n */\nexport function routeError(\n\terror: unknown,\n\t{ statuses = ROUTED }: RouteErrorOptions = {},\n): unknown {\n\tif (isApiErrorLike(error)) {\n\t\tconst { requestId } = error;\n\t\tif (statuses.has(error.status)) {\n\t\t\treturn data<RouteErrorData>(\n\t\t\t\t{ code: error.code, message: error.message, requestId },\n\t\t\t\t{ status: error.status },\n\t\t\t);\n\t\t}\n\t\treturn requestId\n\t\t\t? data<RouteErrorData>({ requestId }, { status: 500 })\n\t\t\t: error;\n\t}\n\tif (isProblemErrorLike(error)) {\n\t\tconst { requestId } = error;\n\t\tif (statuses.has(error.status)) {\n\t\t\treturn data<RouteErrorData>(\n\t\t\t\t{ message: error.message, requestId },\n\t\t\t\t{ status: error.status },\n\t\t\t);\n\t\t}\n\t\treturn requestId\n\t\t\t? data<RouteErrorData>({ requestId }, { status: 500 })\n\t\t\t: error;\n\t}\n\tif (error instanceof Response && statuses.has(error.status)) {\n\t\tconst requestId = requestIdOf(error);\n\t\tif (requestId) {\n\t\t\treturn data<RouteErrorData>({ requestId }, { status: error.status });\n\t\t}\n\t}\n\treturn error;\n}\n\n/**\n * The support reference an error page shows: the API's `X-Request-Id` a\n * route error carries (`routeError`), if it carries one.\n */\nexport function errorRequestId(error: unknown): string | undefined {\n\tif (typeof error !== 'object' || error === null) return undefined;\n\tconst { data: body } = error as { data?: unknown };\n\tif (typeof body !== 'object' || body === null) return undefined;\n\tconst { requestId } = body as { requestId?: unknown };\n\treturn typeof requestId === 'string' && requestId ? requestId : undefined;\n}\n",
7
7
  "import { isNamed } from './api-error';\n\n/** A reply of a REST/OpenAPI client, as far as `problemError` reads it: `{ status, data }`. */\nexport interface ProblemReply {\n\treadonly status: number;\n\t/** The parsed body; a problem document (RFC 9457) when the API refused. */\n\treadonly data?: unknown;\n}\n\nexport interface ProblemErrorOptions {\n\t/** The `x-request-id` the API answered (`requestIdOf(response)`). */\n\trequestId?: string | undefined;\n}\n\n/** A refusal read from a REST API's RFC 9457 problem reply. */\nexport interface ProblemErrorLike extends Error {\n\t/** The HTTP status of the reply. */\n\treadonly status: number;\n\t/** The problem's `title`, else `Error <status>`. */\n\treadonly title: string;\n\t/** The problem's `detail`, when it has one. */\n\treadonly detail?: string | undefined;\n\t/** The problem's `type` URI, when it has one. */\n\treadonly type?: string | undefined;\n\t/** The problem's `instance` URI, when it has one. */\n\treadonly instance?: string | undefined;\n\t/** The id the API logged the request under, when the caller kept it. */\n\trequestId?: string | undefined;\n}\n\nconst text = (value: unknown): string | undefined =>\n\ttypeof value === 'string' && value ? value : undefined;\n\n/**\n * The error of an RFC 9457 problem reply. Its `message` is the problem's\n * `detail`, else its `title`, else `Error <status>`.\n */\nexport class ProblemError extends Error implements ProblemErrorLike {\n\toverride readonly name = 'ProblemError';\n\treadonly status: number;\n\treadonly title: string;\n\treadonly detail: string | undefined;\n\treadonly type: string | undefined;\n\treadonly instance: string | undefined;\n\trequestId: string | undefined;\n\n\tconstructor(reply: ProblemReply, { requestId }: ProblemErrorOptions = {}) {\n\t\tconst body =\n\t\t\ttypeof reply.data === 'object' && reply.data !== null\n\t\t\t\t? (reply.data as {\n\t\t\t\t\t\ttitle?: unknown;\n\t\t\t\t\t\tdetail?: unknown;\n\t\t\t\t\t\ttype?: unknown;\n\t\t\t\t\t\tinstance?: unknown;\n\t\t\t\t\t})\n\t\t\t\t: {};\n\t\tconst title = text(body.title) ?? `Error ${reply.status}`;\n\t\tconst detail = text(body.detail);\n\t\tsuper(detail ?? title);\n\t\tthis.status = reply.status;\n\t\tthis.title = title;\n\t\tthis.detail = detail;\n\t\tthis.type = text(body.type);\n\t\tthis.instance = text(body.instance);\n\t\tthis.requestId = requestId;\n\t}\n}\n\n/**\n * Turns a REST/OpenAPI client's `{ status, data }` reply into a\n * `ProblemError`; a body that is not a problem document gives `Error <status>`.\n */\nexport function problemError(\n\treply: ProblemReply,\n\toptions?: ProblemErrorOptions,\n): ProblemError {\n\treturn new ProblemError(reply, options);\n}\n\n/**\n * Whether `error` is a `ProblemError`, told by its name and a numeric\n * `status`, never `instanceof`, so it holds across two copies of the package.\n */\nexport function isProblemErrorLike(error: unknown): error is ProblemErrorLike {\n\treturn (\n\t\tisNamed(error, 'ProblemError') &&\n\t\ttypeof (error as { status?: unknown }).status === 'number'\n\t);\n}\n",
8
- "/** The header an API echoes on every response: the id its logs carry. */\nexport const REQUEST_ID_HEADER = 'x-request-id';\n\n/** The request id a response carries, if the API sent one. */\nexport function requestIdOf(response: Response): string | undefined {\n\treturn response.headers.get(REQUEST_ID_HEADER) || undefined;\n}\n",
9
- "/**\n * The page a request is for, as `next` carries it back after sign-in: its\n * path and search, without React Router's single-fetch details. A client\n * navigation's `request.url` is `/todos/demo/list.data?q=a&_routes=…`\n * (`/_.data` for `/`, `?index` for an index action), as the server\n * runtime's own `normalizeUrl` strips them.\n */\nexport function nextOf(url: string): string {\n\tconst { pathname, searchParams } = new URL(url);\n\tconst trimmed = pathname.endsWith('/_.data')\n\t\t? pathname.slice(0, -'_.data'.length)\n\t\t: pathname.replace(/\\.data$/, '');\n\t// `new URL('http://h/\\\\evil.com')` has the path `//evil.com`: never let a\n\t// `next` start with two slashes, which a redirect reads as another site.\n\tconst path = trimmed.replace(/^\\/{2,}/, '/');\n\tconst params = new URLSearchParams(searchParams);\n\tparams.delete('_routes');\n\t// An empty `index` names the index route; a valued one is the page's own.\n\tfor (const value of params.getAll('index')) {\n\t\tif (!value) params.delete('index', value);\n\t}\n\tconst search = params.toString();\n\treturn search ? `${path}?${search}` : path;\n}\n",
10
- "import { data } from 'react-router';\nimport { isCrossSite } from './cross-site';\n\n/** Refuses a request from another site with a 403; reads headers only. */\nexport type SiteCheck = (request: Request) => void;\n\n/**\n * The same-site rule of `readJson`: an `Origin` that is not the app's own, or\n * a `Sec-Fetch-Site` that is neither `same-origin` nor `none`, is a 403.\n * `origin` is the app's configured origin (or a getter), else the request's.\n */\nexport function siteCheck(\n\torigin?: string | (() => string | undefined) | undefined,\n): SiteCheck {\n\treturn (request) => {\n\t\tconst set = typeof origin === 'function' ? origin() : origin;\n\t\tif (isCrossSite(request, set)) {\n\t\t\tthrow data('Cross-site request refused', { status: 403 });\n\t\t}\n\t};\n}\n\n/**\n * Where a `jsonActions` instance keeps its site check: an enumerable property\n * under a registered symbol, so it survives `{ ...json }` and a second copy of\n * the package. Not public.\n */\nexport const SITE_CHECK = Symbol.for('@alxia/react-router:site-check@1');\n\n/** Puts the check on a `jsonActions` instance. */\nexport function rememberSiteCheck(actions: object, check: SiteCheck): void {\n\tObject.defineProperty(actions, SITE_CHECK, {\n\t\tvalue: check,\n\t\tenumerable: true,\n\t\twritable: false,\n\t});\n}\n\n/** Whether `actions` carries a site check, as every `jsonActions()` instance does. */\nexport const hasSiteCheck = (actions: object): boolean =>\n\ttypeof (actions as Record<symbol, unknown>)[SITE_CHECK] === 'function';\n\n/**\n * Refuses a cross-site request with the rule of the `jsonActions` instance\n * (its configured origin, read at each request); an object without a check\n * gets the request's own origin. It reads headers only, never the body.\n */\nexport function refuseCrossSite(actions: object, request: Request): void {\n\tconst check = (actions as Record<symbol, unknown>)[SITE_CHECK];\n\t(typeof check === 'function' ? (check as SiteCheck) : siteCheck())(request);\n}\n"
8
+ "/** The header an API echoes on every response: the id its logs carry. */\nexport const REQUEST_ID_HEADER = 'x-request-id';\n\n/** The request id a response carries, if the API sent one. */\nexport function requestIdOf(response: Response): string | undefined {\n\treturn response.headers.get(REQUEST_ID_HEADER) || undefined;\n}\n"
11
9
  ],
12
- "mappings": ";;;;;AA+DO,IAAM,UAAU,CAAC,OAAgB,SACvC,iBAAiB,SAAS,MAAM,SAAS;AAQnC,SAAS,eAAc,CAAC,OAAuC;AAAA,EACrE,OACC,QAAQ,OAAO,UAAU,KACzB,MAAM,QAAS,MAA+B,MAAM,KACpD,OAAQ,MAA+B,WAAW;AAAA;AAS7C,SAAS,YAAW,CAC1B,YACS;AAAA,EACT,MAAM,UAAU,YAAY,QAAQ,WAAW,YAAY;AAAA,EAC3D,IAAI,YAAY;AAAA,IAAW,OAAO,KAAK,IAAI,GAAG,OAAO,OAAO,KAAK,CAAC;AAAA,EAClE,MAAM,UAAU,YAAY,QAAQ,cAAc,YAAY;AAAA,EAC9D,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,OAAO,OAAO,IAAI,EAAE,KAAK,CAAC;AAAA;AAGxD,IAAM,kBAAkB;AAGxB,SAAS,SAAS,CAAC,OAAoC;AAAA,EACtD,MAAM,UACL,OAAO,UAAU,YAAY,gBAAgB,KAAK,MAAM,KAAK,CAAC,IAC3D,OAAO,MAAM,KAAK,CAAC,IACnB;AAAA,EACJ,OAAO,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,KAAK,WAAW,IAC1E,KAAK,IAAI,iBAAiB,KAAK,KAAK,OAAO,CAAC,IAAI,IAChD;AAAA;AAWG,SAAS,aAAY,CAAC,OAAyC;AAAA,EACrE,MAAM,aAAa,MAAM,OAAO,IAAI;AAAA,EACpC,OACC,UAAU,YAAY,QAAQ,UAAU,KACxC,UAAU,YAAY,UAAU;AAAA;AAU3B,SAAS,eAAc,CAAC,OAAyC;AAAA,EACvE,MAAM,aAAa,MAAM,OAAO,IAAI;AAAA,EACpC,WAAW,SAAS;AAAA,IACnB,YAAY;AAAA,IACZ,YAAY,QAAQ;AAAA,EACrB,GAAG;AAAA,IACF,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,GAAG;AAAA,MACvE,OAAO;AAAA,IACR;AAAA,EACD;AAAA,EACA;AAAA;AAOM,SAAS,QAAQ,CACvB,YACsC;AAAA,EACtC,IAAI,CAAC;AAAA,IAAY;AAAA,EACjB,OAAO,WAAW,SACf,KAAK,eAAe,WAAW,OAAO,IACtC;AAAA;AAeG,IAAM,gBAAgB,CAAC,UAC7B,QAAQ,OAAO,gBAAgB,KAC/B,OAAQ,MAA+B,WAAW;AAE5C,IAAM,qBAAqB,CACjC,UAEA,QAAQ,OAAO,qBAAqB,KACpC,OAAQ,MAA+B,WAAW;;;AC/KnD;;;AC8BA,IAAM,OAAO,CAAC,UACb,OAAO,UAAU,YAAY,QAAQ,QAAQ;AAAA;AAMvC,MAAM,sBAAqB,MAAkC;AAAA,EACjD,OAAO;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT;AAAA,EAEA,WAAW,CAAC,SAAuB,cAAmC,CAAC,GAAG;AAAA,IACzE,MAAM,OACL,OAAO,MAAM,SAAS,YAAY,MAAM,SAAS,OAC7C,MAAM,OAMP,CAAC;AAAA,IACL,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,SAAS,MAAM;AAAA,IACjD,MAAM,SAAS,KAAK,KAAK,MAAM;AAAA,IAC/B,MAAM,UAAU,KAAK;AAAA,IACrB,KAAK,SAAS,MAAM;AAAA,IACpB,KAAK,QAAQ;AAAA,IACb,KAAK,SAAS;AAAA,IACd,KAAK,OAAO,KAAK,KAAK,IAAI;AAAA,IAC1B,KAAK,WAAW,KAAK,KAAK,QAAQ;AAAA,IAClC,KAAK,YAAY;AAAA;AAEnB;AAMO,SAAS,aAAY,CAC3B,OACA,SACe;AAAA,EACf,OAAO,IAAI,cAAa,OAAO,OAAO;AAAA;AAOhC,SAAS,mBAAkB,CAAC,OAA2C;AAAA,EAC7E,OACC,QAAQ,OAAO,cAAc,KAC7B,OAAQ,MAA+B,WAAW;AAAA;;;ACrF7C,IAAM,qBAAoB;AAG1B,SAAS,YAAW,CAAC,UAAwC;AAAA,EACnE,OAAO,SAAS,QAAQ,IAAI,kBAAiB,KAAK;AAAA;;;AFCnD,IAAM,SAA8B,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,GAAG,CAAC;AA8BzD,SAAS,WAAU,CACzB,SACE,WAAW,WAA8B,CAAC,GAClC;AAAA,EACV,IAAI,gBAAe,KAAK,GAAG;AAAA,IAC1B,QAAQ,cAAc;AAAA,IACtB,IAAI,SAAS,IAAI,MAAM,MAAM,GAAG;AAAA,MAC/B,OAAO,KACN,EAAE,MAAM,MAAM,MAAM,SAAS,MAAM,SAAS,UAAU,GACtD,EAAE,QAAQ,MAAM,OAAO,CACxB;AAAA,IACD;AAAA,IACA,OAAO,YACJ,KAAqB,EAAE,UAAU,GAAG,EAAE,QAAQ,IAAI,CAAC,IACnD;AAAA,EACJ;AAAA,EACA,IAAI,oBAAmB,KAAK,GAAG;AAAA,IAC9B,QAAQ,cAAc;AAAA,IACtB,IAAI,SAAS,IAAI,MAAM,MAAM,GAAG;AAAA,MAC/B,OAAO,KACN,EAAE,SAAS,MAAM,SAAS,UAAU,GACpC,EAAE,QAAQ,MAAM,OAAO,CACxB;AAAA,IACD;AAAA,IACA,OAAO,YACJ,KAAqB,EAAE,UAAU,GAAG,EAAE,QAAQ,IAAI,CAAC,IACnD;AAAA,EACJ;AAAA,EACA,IAAI,iBAAiB,YAAY,SAAS,IAAI,MAAM,MAAM,GAAG;AAAA,IAC5D,MAAM,YAAY,aAAY,KAAK;AAAA,IACnC,IAAI,WAAW;AAAA,MACd,OAAO,KAAqB,EAAE,UAAU,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,IACpE;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAOD,SAAS,eAAc,CAAC,OAAoC;AAAA,EAClE,IAAI,OAAO,UAAU,YAAY,UAAU;AAAA,IAAM;AAAA,EACjD,QAAQ,MAAM,SAAS;AAAA,EACvB,IAAI,OAAO,SAAS,YAAY,SAAS;AAAA,IAAM;AAAA,EAC/C,QAAQ,cAAc;AAAA,EACtB,OAAO,OAAO,cAAc,YAAY,YAAY,YAAY;AAAA;;;AG3E1D,SAAS,OAAM,CAAC,KAAqB;AAAA,EAC3C,QAAQ,UAAU,iBAAiB,IAAI,IAAI,GAAG;AAAA,EAC9C,MAAM,UAAU,SAAS,SAAS,SAAS,IACxC,SAAS,MAAM,GAAG,CAAC,SAAS,MAAM,IAClC,SAAS,QAAQ,WAAW,EAAE;AAAA,EAGjC,MAAM,OAAO,QAAQ,QAAQ,WAAW,GAAG;AAAA,EAC3C,MAAM,SAAS,IAAI,gBAAgB,YAAY;AAAA,EAC/C,OAAO,OAAO,SAAS;AAAA,EAEvB,WAAW,SAAS,OAAO,OAAO,OAAO,GAAG;AAAA,IAC3C,IAAI,CAAC;AAAA,MAAO,OAAO,OAAO,SAAS,KAAK;AAAA,EACzC;AAAA,EACA,MAAM,SAAS,OAAO,SAAS;AAAA,EAC/B,OAAO,SAAS,GAAG,QAAQ,WAAW;AAAA;;;ACtBvC,iBAAS;AAWF,SAAS,SAAS,CACxB,QACY;AAAA,EACZ,OAAO,CAAC,YAAY;AAAA,IACnB,MAAM,MAAM,OAAO,WAAW,aAAa,OAAO,IAAI;AAAA,IACtD,IAAI,YAAY,SAAS,GAAG,GAAG;AAAA,MAC9B,MAAM,MAAK,8BAA8B,EAAE,QAAQ,IAAI,CAAC;AAAA,IACzD;AAAA;AAAA;AASK,IAAM,aAAa,OAAO,IAAI,kCAAkC;AAGhE,SAAS,iBAAiB,CAAC,SAAiB,OAAwB;AAAA,EAC1E,OAAO,eAAe,SAAS,YAAY;AAAA,IAC1C,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,UAAU;AAAA,EACX,CAAC;AAAA;AAIK,IAAM,eAAe,CAAC,YAC5B,OAAQ,QAAoC,gBAAgB;AAOtD,SAAS,eAAe,CAAC,SAAiB,SAAwB;AAAA,EACxE,MAAM,QAAS,QAAoC;AAAA,GAClD,OAAO,UAAU,aAAc,QAAsB,UAAU,GAAG,OAAO;AAAA;",
13
- "debugId": "A88B070738C15B2B64756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "CDA3CA2B382E0AF864756E2164756E21",
14
12
  "names": []
15
13
  }