@alxia/react-router 0.41.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 (42) hide show
  1. package/README.md +69 -1
  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.js +14 -28
  17. package/dist/fetcher/index.js.map +3 -4
  18. package/dist/oauth/answer.d.ts +24 -0
  19. package/dist/oauth/answer.d.ts.map +1 -0
  20. package/dist/oauth/consent.d.ts +88 -0
  21. package/dist/oauth/consent.d.ts.map +1 -0
  22. package/dist/oauth/index.d.ts +12 -0
  23. package/dist/oauth/index.d.ts.map +1 -0
  24. package/dist/oauth/index.js +106 -0
  25. package/dist/oauth/index.js.map +11 -0
  26. package/dist/oauth/model.d.ts +13 -0
  27. package/dist/oauth/model.d.ts.map +1 -0
  28. package/dist/oauth/react/index.d.ts +7 -0
  29. package/dist/oauth/react/index.d.ts.map +1 -0
  30. package/dist/oauth/react/index.js +87 -0
  31. package/dist/oauth/react/index.js.map +10 -0
  32. package/dist/oauth/react/use-consent.d.ts +47 -0
  33. package/dist/oauth/react/use-consent.d.ts.map +1 -0
  34. package/dist/session/index.js +30 -39
  35. package/dist/session/index.js.map +3 -5
  36. package/dist/session/react/index.js +31 -204
  37. package/dist/session/react/index.js.map +3 -8
  38. package/docs/README.md +2 -1
  39. package/docs/guide/oauth.md +132 -0
  40. package/docs/roadmap.md +17 -1
  41. package/docs/troubleshooting.md +39 -0
  42. package/package.json +14 -2
@@ -0,0 +1,106 @@
1
+ import {
2
+ isApiErrorLike2
3
+ } from "../chunks/index-hn6htqc2.js";
4
+ import {
5
+ fetcherRefusal
6
+ } from "../chunks/index-gchzf3mc.js";
7
+ import {
8
+ STEP_UP_REQUIRED2
9
+ } from "../chunks/index-vr82typq.js";
10
+
11
+ // src/oauth/consent.ts
12
+ import { data } from "react-router";
13
+
14
+ // src/oauth/model.ts
15
+ function requestIdOf(url) {
16
+ const id = url.searchParams.get("request_id") ?? url.searchParams.get("request");
17
+ const trimmed = id?.trim();
18
+ return trimmed ? trimmed : undefined;
19
+ }
20
+ function followableRedirect(value) {
21
+ if (typeof value !== "string")
22
+ return;
23
+ let url;
24
+ try {
25
+ url = new URL(value);
26
+ } catch {
27
+ return;
28
+ }
29
+ return url.protocol === "https:" || url.protocol === "http:" ? url.href : undefined;
30
+ }
31
+
32
+ // src/oauth/consent.ts
33
+ var isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0);
34
+ function inputOf(body) {
35
+ const raw = typeof body === "object" && body !== null ? body : {};
36
+ const { intent, id, scopes } = raw;
37
+ if (intent !== "approve" && intent !== "deny" || typeof id !== "string" || id.length === 0 || scopes !== undefined && !isStrings(scopes)) {
38
+ throw data("Bad consent request", { status: 400 });
39
+ }
40
+ return { intent, id, scopes };
41
+ }
42
+ function oauthConsent(options) {
43
+ const notFound = options.notFound ?? ((error) => error.status === 404);
44
+ const stepUpCode = options.stepUpRequired ?? STEP_UP_REQUIRED2;
45
+ async function loader(request) {
46
+ const { viewer, headers: viewerHeaders } = await options.viewer(request);
47
+ const headers = new Headers(viewerHeaders);
48
+ headers.set("Cache-Control", "no-store");
49
+ const id = requestIdOf(new URL(request.url));
50
+ let pending = null;
51
+ if (id) {
52
+ try {
53
+ pending = await options.find(id, options.caller(request)) ?? null;
54
+ } catch (error) {
55
+ if (!(isApiErrorLike2(error) && notFound(error)))
56
+ throw error;
57
+ }
58
+ }
59
+ return data({ pending, viewer }, pending ? { headers } : { status: 404, headers });
60
+ }
61
+ async function action(request, language) {
62
+ options.json.requireJsonPost(request);
63
+ const input = inputOf(await options.json.readJson(request));
64
+ const about = { intent: input.intent, id: input.id };
65
+ const caller = options.caller(request);
66
+ try {
67
+ const { redirectTo } = input.intent === "approve" ? await options.approve({
68
+ id: input.id,
69
+ ...input.scopes && { scopes: input.scopes }
70
+ }, caller) : await options.deny({ id: input.id }, caller);
71
+ const next = followableRedirect(redirectTo);
72
+ if (!next) {
73
+ throw new Error("The API answered a redirect that is not an address");
74
+ }
75
+ return { ok: true, ...about, redirectTo: next };
76
+ } catch (error) {
77
+ const code = isApiErrorLike2(error) ? error.code : undefined;
78
+ const { message, status } = fetcherRefusal(options.errors, error, language);
79
+ const stepUp = code === stepUpCode;
80
+ const expired = isApiErrorLike2(error) && !stepUp && notFound(error);
81
+ return data({
82
+ ok: false,
83
+ ...about,
84
+ stepUp,
85
+ expired,
86
+ error: stepUp || expired ? "" : message
87
+ }, { status });
88
+ }
89
+ }
90
+ return { loader, action };
91
+ }
92
+ function consentShouldRevalidate({
93
+ formMethod,
94
+ defaultShouldRevalidate
95
+ }) {
96
+ return formMethod ? false : defaultShouldRevalidate;
97
+ }
98
+ export {
99
+ consentShouldRevalidate,
100
+ followableRedirect,
101
+ oauthConsent,
102
+ requestIdOf
103
+ };
104
+
105
+ //# debugId=DB3702F6926B73AE64756E2164756E21
106
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/oauth/consent.ts", "../src/oauth/model.ts"],
4
+ "sourcesContent": [
5
+ "import { data, type ShouldRevalidateFunctionArgs } from 'react-router';\nimport { type ApiErrorLike, isApiErrorLike } from '../errors/api-error';\nimport type { ApiErrors } from '../errors/api-errors';\nimport { fetcherRefusal } from '../errors/fetcher-result';\nimport type { JsonActions } from '../errors/json-actions';\nimport { STEP_UP_REQUIRED } from '../session/step-up-codes';\nimport type { ConsentAnswer, ConsentDecisionIntent } from './answer';\nimport { followableRedirect, requestIdOf } from './model';\n\n/** Where a decided request sends the browser, as `OAuthDecision` of `@nxgt/janus-oauth/graphql` has it. */\nexport interface ConsentRedirect {\n\treadonly redirectTo: string;\n}\n\n/** The decision a page posted, once checked. */\nexport interface ConsentInput {\n\treadonly intent: ConsentDecisionIntent;\n\treadonly id: string;\n\t/** The scopes allowed; all the asked ones when absent. */\n\treadonly scopes?: readonly string[] | undefined;\n}\n\nexport interface ConsentOptions<\n\tL extends string,\n\tCaller,\n\tViewer,\n\tPending extends object,\n> {\n\t/** The app's `jsonActions()`: the same-site and JSON checks of the action. */\n\treadonly json: Pick<JsonActions, 'readJson' | 'requireJsonPost'>;\n\t/** The app's `createApiErrors()`: words the refusals. */\n\treadonly errors: Pick<ApiErrors<L>, 'apiRefusal'>;\n\t/**\n\t * The signed-in viewer, or a thrown redirect to sign in that comes back to\n\t * this very address, request id included (`SessionFlow.signInRedirect`\n\t * does). `headers` are added to the answer (a refreshed session cookie).\n\t */\n\tviewer(\n\t\trequest: Request,\n\t): Promise<{ readonly viewer: Viewer; readonly headers?: HeadersInit }>;\n\t/** The caller that carries the request's session to the API. */\n\tcaller(request: Request): Caller;\n\t/** `oauthRequest(id)`: the pending request, or `null` when the API has none. */\n\tfind(id: string, caller: Caller): Promise<Pending | null | undefined>;\n\t/** `approveOAuthRequest(id, scopes)`. */\n\tapprove(\n\t\tinput: { readonly id: string; readonly scopes?: readonly string[] },\n\t\tcaller: Caller,\n\t): Promise<ConsentRedirect>;\n\t/** `denyOAuthRequest(id)`. */\n\tdeny(\n\t\tinput: { readonly id: string },\n\t\tcaller: Caller,\n\t): Promise<ConsentRedirect>;\n\t/**\n\t * Whether an API refusal means the request is unknown, decided, lapsed or\n\t * not the viewer's (the API cannot tell which). Default: a 404.\n\t */\n\tnotFound?(error: ApiErrorLike): boolean;\n\t/** The API's step-up code. Default `STEP_UP_REQUIRED`. */\n\tstepUpRequired?: string;\n}\n\nexport interface ConsentRoute<\n\tL extends string,\n\tViewer,\n\tPending extends object,\n> {\n\t/**\n\t * The route's loader: the viewer and the pending request (`null` for an\n\t * unknown one or none in the address, answered with a 404 so the page shows\n\t * its expired state). Never cached: it names a one-time request.\n\t */\n\tloader(\n\t\trequest: Request,\n\t): Promise<\n\t\tReturnType<typeof data<{ pending: Pending | null; viewer: Viewer }>>\n\t>;\n\t/**\n\t * The route's action: approves or denies the JSON `{ intent, id, scopes? }`\n\t * and answers a `ConsentAnswer` for the page's fetcher.\n\t */\n\taction(\n\t\trequest: Request,\n\t\tlanguage: L,\n\t): Promise<ConsentAnswer | ReturnType<typeof data<ConsentAnswer>>>;\n}\n\nconst isStrings = (value: unknown): value is string[] =>\n\tArray.isArray(value) &&\n\tvalue.every((item) => typeof item === 'string' && item.length > 0);\n\nfunction inputOf(body: unknown): ConsentInput {\n\tconst raw = (typeof body === 'object' && body !== null ? body : {}) as Record<\n\t\tstring,\n\t\tunknown\n\t>;\n\tconst { intent, id, scopes } = raw;\n\tif (\n\t\t(intent !== 'approve' && intent !== 'deny') ||\n\t\ttypeof id !== 'string' ||\n\t\tid.length === 0 ||\n\t\t(scopes !== undefined && !isStrings(scopes))\n\t) {\n\t\tthrow data('Bad consent request', { status: 400 });\n\t}\n\treturn { intent, id, scopes };\n}\n\n/**\n * The server side of the screen where a viewer allows or denies an OAuth\n * client (`oauthRequest`, `approveOAuthRequest` and `denyOAuthRequest` of\n * `@nxgt/janus-oauth/graphql`): a loader and an action for one route, the\n * GraphQL calls passed in. The page reads `useConsent`\n * (`@alxia/react-router/oauth/react`).\n *\n * @example\n * const consent = oauthConsent({ json, errors, viewer, caller, find, approve, deny });\n * export const loader = ({ request }) => consent.loader(request);\n * export const action = ({ request }) => consent.action(request, language);\n */\nexport function oauthConsent<\n\tL extends string,\n\tCaller,\n\tViewer,\n\tPending extends object,\n>(\n\toptions: ConsentOptions<L, Caller, Viewer, Pending>,\n): ConsentRoute<L, Viewer, Pending> {\n\tconst notFound =\n\t\toptions.notFound ?? ((error: ApiErrorLike) => error.status === 404);\n\tconst stepUpCode = options.stepUpRequired ?? STEP_UP_REQUIRED;\n\n\tasync function loader(request: Request) {\n\t\tconst { viewer, headers: viewerHeaders } = await options.viewer(request);\n\t\tconst headers = new Headers(viewerHeaders);\n\t\theaders.set('Cache-Control', 'no-store');\n\t\tconst id = requestIdOf(new URL(request.url));\n\t\tlet pending: Pending | null = null;\n\t\tif (id) {\n\t\t\ttry {\n\t\t\t\tpending = (await options.find(id, options.caller(request))) ?? null;\n\t\t\t} catch (error) {\n\t\t\t\tif (!(isApiErrorLike(error) && notFound(error))) throw error;\n\t\t\t}\n\t\t}\n\t\treturn data(\n\t\t\t{ pending, viewer },\n\t\t\tpending ? { headers } : { status: 404, headers },\n\t\t);\n\t}\n\n\tasync function action(request: Request, language: L) {\n\t\toptions.json.requireJsonPost(request);\n\t\tconst input = inputOf(await options.json.readJson(request));\n\t\tconst about = { intent: input.intent, id: input.id } as const;\n\t\tconst caller = options.caller(request);\n\t\ttry {\n\t\t\tconst { redirectTo } =\n\t\t\t\tinput.intent === 'approve'\n\t\t\t\t\t? await options.approve(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tid: input.id,\n\t\t\t\t\t\t\t\t...(input.scopes && { scopes: input.scopes }),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcaller,\n\t\t\t\t\t\t)\n\t\t\t\t\t: await options.deny({ id: input.id }, caller);\n\t\t\tconst next = followableRedirect(redirectTo);\n\t\t\tif (!next) {\n\t\t\t\tthrow new Error('The API answered a redirect that is not an address');\n\t\t\t}\n\t\t\treturn { ok: true as const, ...about, redirectTo: next };\n\t\t} catch (error) {\n\t\t\tconst code = isApiErrorLike(error) ? error.code : undefined;\n\t\t\tconst { message, status } = fetcherRefusal(\n\t\t\t\toptions.errors,\n\t\t\t\terror,\n\t\t\t\tlanguage,\n\t\t\t);\n\t\t\tconst stepUp = code === stepUpCode;\n\t\t\tconst expired = isApiErrorLike(error) && !stepUp && notFound(error);\n\t\t\treturn data(\n\t\t\t\t{\n\t\t\t\t\tok: false as const,\n\t\t\t\t\t...about,\n\t\t\t\t\tstepUp,\n\t\t\t\t\texpired,\n\t\t\t\t\terror: stepUp || expired ? '' : message,\n\t\t\t\t},\n\t\t\t\t{ status },\n\t\t\t);\n\t\t}\n\t}\n\n\treturn { loader, action };\n}\n\n/**\n * `shouldRevalidate` of the consent route: never reload after a decision.\n * The decision spends the request, so the loader would answer \"expired\"\n * while the browser is on its way to the client's callback. Any other change\n * (another `request_id`, a navigation) reloads as usual.\n */\nexport function consentShouldRevalidate({\n\tformMethod,\n\tdefaultShouldRevalidate,\n}: Pick<\n\tShouldRevalidateFunctionArgs,\n\t'formMethod' | 'defaultShouldRevalidate'\n>): boolean {\n\treturn formMethod ? false : defaultShouldRevalidate;\n}\n",
6
+ "/**\n * The request an authorization link names: `request_id` (what the API sends\n * the browser with), else `request`. Empty or absent: `undefined`.\n */\nexport function requestIdOf(url: URL): string | undefined {\n\tconst id =\n\t\turl.searchParams.get('request_id') ?? url.searchParams.get('request');\n\tconst trimmed = id?.trim();\n\treturn trimmed ? trimmed : undefined;\n}\n\n/**\n * Where the browser is sent once a request is decided: the client's own\n * callback, normalised. Only http(s) is followed (a loopback callback is\n * http), never a `javascript:` or `data:` address. Anything else:\n * `undefined`.\n */\nexport function followableRedirect(value: unknown): string | undefined {\n\tif (typeof value !== 'string') return undefined;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(value);\n\t} catch {\n\t\treturn undefined;\n\t}\n\treturn url.protocol === 'https:' || url.protocol === 'http:'\n\t\t? url.href\n\t\t: undefined;\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "DB3702F6926B73AE64756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The request an authorization link names: `request_id` (what the API sends
3
+ * the browser with), else `request`. Empty or absent: `undefined`.
4
+ */
5
+ export declare function requestIdOf(url: URL): string | undefined;
6
+ /**
7
+ * Where the browser is sent once a request is decided: the client's own
8
+ * callback, normalised. Only http(s) is followed (a loopback callback is
9
+ * http), never a `javascript:` or `data:` address. Anything else:
10
+ * `undefined`.
11
+ */
12
+ export declare function followableRedirect(value: unknown): string | undefined;
13
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/oauth/model.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAKxD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAWrE"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `@alxia/react-router/oauth/react`: the hook of the screen where a viewer
3
+ * allows or denies an OAuth client. `react` and `react-router` are its only
4
+ * imports; it shares nothing with the server side of `/oauth`.
5
+ */
6
+ export { type UseConsentOptions, useConsent } from './use-consent';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/oauth/react/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,87 @@
1
+ import {
2
+ useFetcherAnswer2
3
+ } from "../../chunks/index-zyg8amjc.js";
4
+ import {
5
+ useStepUpReplay2
6
+ } from "../../chunks/index-1f16k1sj.js";
7
+
8
+ // src/oauth/react/use-consent.ts
9
+ import { useRef, useState } from "react";
10
+ import { useFetcher } from "react-router";
11
+ function useConsent(requestId, { go, action, request, confirm }) {
12
+ const fetcher = useFetcher({
13
+ key: `oauth-consent-${requestId}`
14
+ });
15
+ const busy = fetcher.state !== "idle";
16
+ const last = useRef(undefined);
17
+ const [error, setError] = useState(undefined);
18
+ const [expired, setExpired] = useState(false);
19
+ const [leaving, setLeaving] = useState(false);
20
+ const [shownFor, setShownFor] = useState(requestId);
21
+ if (shownFor !== requestId) {
22
+ setShownFor(requestId);
23
+ setError(undefined);
24
+ setExpired(false);
25
+ setLeaving(false);
26
+ last.current = undefined;
27
+ }
28
+ const send = (intent) => fetcher.submit({ intent, id: requestId }, {
29
+ method: "post",
30
+ encType: "application/json",
31
+ ...action ? { action } : {}
32
+ });
33
+ const decide = (intent) => {
34
+ if (busy || leaving)
35
+ return;
36
+ last.current = intent;
37
+ setError(undefined);
38
+ send(intent);
39
+ };
40
+ const latest = useFetcherAnswer2(fetcher, (answer) => {
41
+ if (!answer.ok) {
42
+ if (answer.stepUp)
43
+ return;
44
+ last.current = undefined;
45
+ if (answer.expired)
46
+ setExpired(true);
47
+ else
48
+ setError(answer.error);
49
+ return;
50
+ }
51
+ setLeaving(true);
52
+ (go ?? ((url) => window.location.assign(url)))(answer.redirectTo);
53
+ });
54
+ const stepUp = useStepUpReplay2({
55
+ answer: latest,
56
+ action: last.current,
57
+ replay() {
58
+ if (last.current)
59
+ send(last.current);
60
+ },
61
+ spendsCode: () => false,
62
+ request,
63
+ confirm
64
+ });
65
+ const { dialogProps } = stepUp;
66
+ const pending = busy || leaving || dialogProps.open;
67
+ return {
68
+ approve: () => decide("approve"),
69
+ deny: () => decide("deny"),
70
+ pending: pending ? last.current ?? true : false,
71
+ expired,
72
+ error,
73
+ dialogProps: {
74
+ ...dialogProps,
75
+ onCancel() {
76
+ dialogProps.onCancel();
77
+ last.current = undefined;
78
+ }
79
+ }
80
+ };
81
+ }
82
+ export {
83
+ useConsent
84
+ };
85
+
86
+ //# debugId=8F0D98662C6B0DFC64756E2164756E21
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/oauth/react/use-consent.ts"],
4
+ "sourcesContent": [
5
+ "import { useRef, useState } from 'react';\nimport { useFetcher } from 'react-router';\nimport { useFetcherAnswer } from '../../fetcher/use-fetcher-answer';\nimport {\n\ttype StepUpReplayOptions,\n\tuseStepUpReplay,\n} from '../../session/react/step-up';\nimport type { ConsentAnswer, ConsentDecisionIntent } from '../answer';\n\nexport interface UseConsentOptions\n\textends Pick<\n\t\tStepUpReplayOptions<ConsentDecisionIntent>,\n\t\t'request' | 'confirm'\n\t> {\n\t/**\n\t * Where the browser goes once a decision lands. A hard navigation by\n\t * default (`window.location.assign`): the callback is another site's.\n\t * A parameter so a test can watch it.\n\t */\n\tgo?(url: string): void;\n\t/** The route whose action decides, when it is not the page's own. */\n\taction?: string;\n}\n\n/**\n * The decision of a consent screen, posted as JSON to the page's action\n * (`oauthConsent().action`). A session not confirmed lately opens the shared\n * step-up (`useStepUpReplay`, nothing spent) and the decision goes again by\n * itself; a decision that lands sends the browser to the client's callback\n * (`go`), and the card stays pending until the page is left. A request the\n * API no longer knows is `expired`; any other refusal is the `error`.\n *\n * @example\n * const { approve, deny, pending, expired, error, dialogProps } =\n * \tuseConsent(request.id, { request: stepUpRequest, confirm: stepUpConfirm });\n */\nexport function useConsent(\n\trequestId: string,\n\t{ go, action, request, confirm }: UseConsentOptions,\n) {\n\tconst fetcher = useFetcher<ConsentAnswer>({\n\t\tkey: `oauth-consent-${requestId}`,\n\t});\n\tconst busy = fetcher.state !== 'idle';\n\tconst last = useRef<ConsentDecisionIntent | undefined>(undefined);\n\tconst [error, setError] = useState<string | undefined>(undefined);\n\tconst [expired, setExpired] = useState(false);\n\tconst [leaving, setLeaving] = useState(false);\n\n\t// Another request in the same mounted route starts from a clean card.\n\tconst [shownFor, setShownFor] = useState(requestId);\n\tif (shownFor !== requestId) {\n\t\tsetShownFor(requestId);\n\t\tsetError(undefined);\n\t\tsetExpired(false);\n\t\tsetLeaving(false);\n\t\tlast.current = undefined;\n\t}\n\n\tconst send = (intent: ConsentDecisionIntent) =>\n\t\tfetcher.submit(\n\t\t\t{ intent, id: requestId },\n\t\t\t{\n\t\t\t\tmethod: 'post',\n\t\t\t\tencType: 'application/json',\n\t\t\t\t...(action ? { action } : {}),\n\t\t\t},\n\t\t);\n\n\tconst decide = (intent: ConsentDecisionIntent) => {\n\t\tif (busy || leaving) return;\n\t\tlast.current = intent;\n\t\tsetError(undefined);\n\t\tvoid send(intent);\n\t};\n\n\tconst latest = useFetcherAnswer(fetcher, (answer) => {\n\t\tif (!answer.ok) {\n\t\t\tif (answer.stepUp) return; // the dialog asks; `replay` sends again\n\t\t\tlast.current = undefined;\n\t\t\tif (answer.expired) setExpired(true);\n\t\t\telse setError(answer.error);\n\t\t\treturn;\n\t\t}\n\t\tsetLeaving(true);\n\t\t(go ?? ((url) => window.location.assign(url)))(answer.redirectTo);\n\t});\n\n\tconst stepUp = useStepUpReplay<ConsentDecisionIntent>({\n\t\tanswer: latest,\n\t\taction: last.current,\n\t\treplay() {\n\t\t\tif (last.current) void send(last.current);\n\t\t},\n\t\t// A decision carries no one-time code: nothing was spent.\n\t\tspendsCode: () => false,\n\t\trequest,\n\t\tconfirm,\n\t});\n\n\tconst { dialogProps } = stepUp;\n\t// `last` is a ref, but each write sits beside a state change or a fetcher\n\t// transition, so a render follows it.\n\tconst pending = busy || leaving || dialogProps.open;\n\treturn {\n\t\tapprove: () => decide('approve'),\n\t\tdeny: () => decide('deny'),\n\t\t/** What the card spins: the decision in flight, held until the page is left. */\n\t\tpending: pending ? (last.current ?? true) : (false as const),\n\t\texpired,\n\t\terror,\n\t\t/** The step-up dialog's props; cancelling it frees the card. */\n\t\tdialogProps: {\n\t\t\t...dialogProps,\n\t\t\tonCancel() {\n\t\t\t\tdialogProps.onCancel();\n\t\t\t\tlast.current = undefined;\n\t\t\t},\n\t\t},\n\t};\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;AAAA;AACA;AAmCO,SAAS,UAAU,CACzB,aACE,IAAI,QAAQ,SAAS,WACtB;AAAA,EACD,MAAM,UAAU,WAA0B;AAAA,IACzC,KAAK,iBAAiB;AAAA,EACvB,CAAC;AAAA,EACD,MAAM,OAAO,QAAQ,UAAU;AAAA,EAC/B,MAAM,OAAO,OAA0C,SAAS;AAAA,EAChE,OAAO,OAAO,YAAY,SAA6B,SAAS;AAAA,EAChE,OAAO,SAAS,cAAc,SAAS,KAAK;AAAA,EAC5C,OAAO,SAAS,cAAc,SAAS,KAAK;AAAA,EAG5C,OAAO,UAAU,eAAe,SAAS,SAAS;AAAA,EAClD,IAAI,aAAa,WAAW;AAAA,IAC3B,YAAY,SAAS;AAAA,IACrB,SAAS,SAAS;AAAA,IAClB,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK;AAAA,IAChB,KAAK,UAAU;AAAA,EAChB;AAAA,EAEA,MAAM,OAAO,CAAC,WACb,QAAQ,OACP,EAAE,QAAQ,IAAI,UAAU,GACxB;AAAA,IACC,QAAQ;AAAA,IACR,SAAS;AAAA,OACL,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,EAC5B,CACD;AAAA,EAED,MAAM,SAAS,CAAC,WAAkC;AAAA,IACjD,IAAI,QAAQ;AAAA,MAAS;AAAA,IACrB,KAAK,UAAU;AAAA,IACf,SAAS,SAAS;AAAA,IACb,KAAK,MAAM;AAAA;AAAA,EAGjB,MAAM,SAAS,kBAAiB,SAAS,CAAC,WAAW;AAAA,IACpD,IAAI,CAAC,OAAO,IAAI;AAAA,MACf,IAAI,OAAO;AAAA,QAAQ;AAAA,MACnB,KAAK,UAAU;AAAA,MACf,IAAI,OAAO;AAAA,QAAS,WAAW,IAAI;AAAA,MACnC;AAAA,QAAK,SAAS,OAAO,KAAK;AAAA,MAC1B;AAAA,IACD;AAAA,IACA,WAAW,IAAI;AAAA,KACd,OAAO,CAAC,QAAQ,OAAO,SAAS,OAAO,GAAG,IAAI,OAAO,UAAU;AAAA,GAChE;AAAA,EAED,MAAM,SAAS,iBAAuC;AAAA,IACrD,QAAQ;AAAA,IACR,QAAQ,KAAK;AAAA,IACb,MAAM,GAAG;AAAA,MACR,IAAI,KAAK;AAAA,QAAc,KAAK,KAAK,OAAO;AAAA;AAAA,IAGzC,YAAY,MAAM;AAAA,IAClB;AAAA,IACA;AAAA,EACD,CAAC;AAAA,EAED,QAAQ,gBAAgB;AAAA,EAGxB,MAAM,UAAU,QAAQ,WAAW,YAAY;AAAA,EAC/C,OAAO;AAAA,IACN,SAAS,MAAM,OAAO,SAAS;AAAA,IAC/B,MAAM,MAAM,OAAO,MAAM;AAAA,IAEzB,SAAS,UAAW,KAAK,WAAW,OAAS;AAAA,IAC7C;AAAA,IACA;AAAA,IAEA,aAAa;AAAA,SACT;AAAA,MACH,QAAQ,GAAG;AAAA,QACV,YAAY,SAAS;AAAA,QACrB,KAAK,UAAU;AAAA;AAAA,IAEjB;AAAA,EACD;AAAA;",
8
+ "debugId": "8F0D98662C6B0DFC64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,47 @@
1
+ import { type StepUpReplayOptions } from '../../session/react/step-up';
2
+ import type { ConsentDecisionIntent } from '../answer';
3
+ export interface UseConsentOptions extends Pick<StepUpReplayOptions<ConsentDecisionIntent>, 'request' | 'confirm'> {
4
+ /**
5
+ * Where the browser goes once a decision lands. A hard navigation by
6
+ * default (`window.location.assign`): the callback is another site's.
7
+ * A parameter so a test can watch it.
8
+ */
9
+ go?(url: string): void;
10
+ /** The route whose action decides, when it is not the page's own. */
11
+ action?: string;
12
+ }
13
+ /**
14
+ * The decision of a consent screen, posted as JSON to the page's action
15
+ * (`oauthConsent().action`). A session not confirmed lately opens the shared
16
+ * step-up (`useStepUpReplay`, nothing spent) and the decision goes again by
17
+ * itself; a decision that lands sends the browser to the client's callback
18
+ * (`go`), and the card stays pending until the page is left. A request the
19
+ * API no longer knows is `expired`; any other refusal is the `error`.
20
+ *
21
+ * @example
22
+ * const { approve, deny, pending, expired, error, dialogProps } =
23
+ * useConsent(request.id, { request: stepUpRequest, confirm: stepUpConfirm });
24
+ */
25
+ export declare function useConsent(requestId: string, { go, action, request, confirm }: UseConsentOptions): {
26
+ approve: () => void;
27
+ deny: () => void;
28
+ /** What the card spins: the decision in flight, held until the page is left. */
29
+ pending: boolean | ConsentDecisionIntent;
30
+ expired: boolean;
31
+ error: string | undefined;
32
+ /** The step-up dialog's props; cancelling it frees the card. */
33
+ dialogProps: {
34
+ onCancel(): void;
35
+ open: boolean;
36
+ phase: import("../../session/react").StepUpDialogPhase;
37
+ via: import("../../session/model").StepUpVia | undefined;
38
+ props: import("../../session/model").StepUpProps;
39
+ message: string | undefined;
40
+ busy: boolean;
41
+ confirming: boolean;
42
+ onRetry(): void;
43
+ onSubmit(code: string): void;
44
+ onResend: (() => void) | undefined;
45
+ };
46
+ };
47
+ //# sourceMappingURL=use-consent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-consent.d.ts","sourceRoot":"","sources":["../../../src/oauth/react/use-consent.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,KAAK,mBAAmB,EAExB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAiB,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAEtE,MAAM,WAAW,iBAChB,SAAQ,IAAI,CACX,mBAAmB,CAAC,qBAAqB,CAAC,EAC1C,SAAS,GAAG,SAAS,CACrB;IACD;;;;OAIG;IACH,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACzB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,iBAAiB;;;IAqElD,gFAAgF;;;;IAIhF,gEAAgE;;;;;;;;;;;;;;EASjE"}
@@ -1,12 +1,8 @@
1
1
  import {
2
- isApiErrorLike2,
3
- retryAfterOf2,
4
- attemptsLeftOf2,
5
- routeError2,
6
2
  nextOf2,
7
3
  hasSiteCheck,
8
4
  refuseCrossSite
9
- } from "../chunks/index-2swe47m6.js";
5
+ } from "../chunks/index-atxcmy0f.js";
10
6
  import {
11
7
  HOME2,
12
8
  safeNext2,
@@ -15,16 +11,20 @@ import {
15
11
  signInPath2,
16
12
  secondFactorPath2
17
13
  } from "../chunks/index-xvx63t7n.js";
18
-
19
- // src/session/codes.ts
20
- var INVALID_CODE = "CODE_INVALID";
21
- var CHALLENGE_OVER = [
22
- "TOKEN_UNKNOWN",
23
- "TOKEN_SPENT",
24
- "TOKEN_EXPIRED"
25
- ];
26
- var MAIL_THROTTLED = "MAIL_THROTTLED";
27
- var UNAUTHENTICATED = "UNAUTHENTICATED";
14
+ import {
15
+ isApiErrorLike2,
16
+ retryAfterOf2,
17
+ attemptsLeftOf2,
18
+ routeError2
19
+ } from "../chunks/index-hn6htqc2.js";
20
+ import {
21
+ INVALID_CODE2,
22
+ CHALLENGE_OVER2,
23
+ MAIL_THROTTLED2,
24
+ UNAUTHENTICATED2,
25
+ STEP_UP_REQUIRED2,
26
+ STEP_UP_OVER2
27
+ } from "../chunks/index-vr82typq.js";
28
28
  // src/session/serialize.ts
29
29
  var SAME_SITE = { lax: "Lax", strict: "Strict", none: "None" };
30
30
  function checkSameSite(caller, sameSite, secure) {
@@ -349,7 +349,7 @@ async function afterReset(options, resetDone, request, { context } = {}) {
349
349
  var RESET_DONE = "/sign-in?password-reset=1";
350
350
  var worded = ({ wording, errors }, error, language) => wording?.(error, language) ?? errors.apiErrorMessage(error, language);
351
351
  var throttleCodes = (codes) => {
352
- const configured = codes?.mailThrottled ?? MAIL_THROTTLED;
352
+ const configured = codes?.mailThrottled ?? MAIL_THROTTLED2;
353
353
  return typeof configured === "string" ? [configured] : configured;
354
354
  };
355
355
  async function requestLink(options, run, { request, language }) {
@@ -536,8 +536,8 @@ function resolve(options) {
536
536
  if (!hasSiteCheck(options.json)) {
537
537
  throw new TypeError("signInFlow(): json must come from jsonActions(), which carries the site check every step runs; this object has none (a hand-made double?). Pass the app's jsonActions({ origin }) instance, or a spread copy of it.");
538
538
  }
539
- const invalidCode = options.codes?.invalidCode ?? INVALID_CODE;
540
- const challengeOver = options.codes?.challengeOver ?? CHALLENGE_OVER;
539
+ const invalidCode = options.codes?.invalidCode ?? INVALID_CODE2;
540
+ const challengeOver = options.codes?.challengeOver ?? CHALLENGE_OVER2;
541
541
  if (challengeOver.includes(invalidCode)) {
542
542
  throw new TypeError(`signInFlow(): the code ${JSON.stringify(invalidCode)} is both the wrong-code code (codes.invalidCode) and in codes.challengeOver. A wrong code that leaves tries cannot end the challenge.`);
543
543
  }
@@ -639,15 +639,6 @@ function signedSession(options) {
639
639
  // src/session/step-up.ts
640
640
  import { data as data6 } from "react-router";
641
641
 
642
- // src/session/step-up-codes.ts
643
- var STEP_UP_REQUIRED = "STEP_UP_REQUIRED";
644
- var STEP_UP_OVER = [
645
- ...CHALLENGE_OVER,
646
- "TOKEN_STALE",
647
- "SECOND_FACTOR_ACTIVE",
648
- "SECOND_FACTOR_NOT_ENROLLED"
649
- ];
650
-
651
642
  // src/session/step-up-signed-out.ts
652
643
  function isSignedOut(error, unauthenticated, from) {
653
644
  if (error instanceof Response)
@@ -705,8 +696,8 @@ async function signingOut({
705
696
 
706
697
  // src/session/step-up.ts
707
698
  function resolve2(steps) {
708
- const invalidCode = steps.codes?.invalidCode ?? INVALID_CODE;
709
- const challengeOver = steps.codes?.challengeOver ?? STEP_UP_OVER;
699
+ const invalidCode = steps.codes?.invalidCode ?? INVALID_CODE2;
700
+ const challengeOver = steps.codes?.challengeOver ?? STEP_UP_OVER2;
710
701
  if (challengeOver.includes(invalidCode)) {
711
702
  throw new TypeError(`stepUpSteps(): the code ${JSON.stringify(invalidCode)} is both the wrong-code code (codes.invalidCode) and in codes.challengeOver. A wrong code that leaves tries cannot end the challenge.`);
712
703
  }
@@ -720,9 +711,9 @@ function resolve2(steps) {
720
711
  steps,
721
712
  invalidCode,
722
713
  challengeOver,
723
- stepUpRequired: steps.codes?.stepUpRequired ?? STEP_UP_REQUIRED,
724
- mailThrottled: steps.codes?.mailThrottled ?? MAIL_THROTTLED,
725
- unauthenticated: steps.codes?.unauthenticated ?? UNAUTHENTICATED
714
+ stepUpRequired: steps.codes?.stepUpRequired ?? STEP_UP_REQUIRED2,
715
+ mailThrottled: steps.codes?.mailThrottled ?? MAIL_THROTTLED2,
716
+ unauthenticated: steps.codes?.unauthenticated ?? UNAUTHENTICATED2
726
717
  };
727
718
  }
728
719
  function worded2({ steps }, error, language) {
@@ -963,13 +954,13 @@ function createSignInThrottle(options = {}) {
963
954
  };
964
955
  }
965
956
  export {
966
- CHALLENGE_OVER,
967
- INVALID_CODE,
968
- MAIL_THROTTLED,
969
- STEP_UP_OVER,
970
- STEP_UP_REQUIRED,
957
+ CHALLENGE_OVER2 as CHALLENGE_OVER,
958
+ INVALID_CODE2 as INVALID_CODE,
959
+ MAIL_THROTTLED2 as MAIL_THROTTLED,
960
+ STEP_UP_OVER2 as STEP_UP_OVER,
961
+ STEP_UP_REQUIRED2 as STEP_UP_REQUIRED,
971
962
  TOKEN_PAGE_HEADERS,
972
- UNAUTHENTICATED,
963
+ UNAUTHENTICATED2 as UNAUTHENTICATED,
973
964
  authLinks,
974
965
  createSignInThrottle,
975
966
  readCookie,
@@ -981,5 +972,5 @@ export {
981
972
  stepUpSteps
982
973
  };
983
974
 
984
- //# debugId=E267BD0EB46649CD64756E2164756E21
975
+ //# debugId=5F046EFEF4108A2264756E2164756E21
985
976
  //# sourceMappingURL=index.js.map