@alxia/react-router 0.13.0 → 0.15.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 (76) hide show
  1. package/README.md +78 -0
  2. package/dist/chunks/index-0tyw54ad.js +160 -0
  3. package/dist/chunks/index-0tyw54ad.js.map +15 -0
  4. package/dist/chunks/index-c3k47yw7.js +124 -0
  5. package/dist/chunks/index-c3k47yw7.js.map +14 -0
  6. package/dist/errors/api-error.d.ts +18 -0
  7. package/dist/errors/api-error.d.ts.map +1 -1
  8. package/dist/errors/index.d.ts +1 -1
  9. package/dist/errors/index.d.ts.map +1 -1
  10. package/dist/errors/index.js +45 -110
  11. package/dist/errors/index.js.map +4 -8
  12. package/dist/errors/json-actions.d.ts.map +1 -1
  13. package/dist/errors/same-site.d.ts +25 -0
  14. package/dist/errors/same-site.d.ts.map +1 -0
  15. package/dist/session/codes.d.ts +7 -0
  16. package/dist/session/codes.d.ts.map +1 -0
  17. package/dist/session/cookies.d.ts +60 -0
  18. package/dist/session/cookies.d.ts.map +1 -0
  19. package/dist/session/flow-types.d.ts +194 -0
  20. package/dist/session/flow-types.d.ts.map +1 -0
  21. package/dist/session/guard.d.ts +10 -0
  22. package/dist/session/guard.d.ts.map +1 -0
  23. package/dist/session/index.d.ts +18 -0
  24. package/dist/session/index.d.ts.map +1 -0
  25. package/dist/session/index.js +648 -0
  26. package/dist/session/index.js.map +23 -0
  27. package/dist/session/land.d.ts +28 -0
  28. package/dist/session/land.d.ts.map +1 -0
  29. package/dist/session/link-land.d.ts +20 -0
  30. package/dist/session/link-land.d.ts.map +1 -0
  31. package/dist/session/link-token.d.ts +22 -0
  32. package/dist/session/link-token.d.ts.map +1 -0
  33. package/dist/session/link-types.d.ts +128 -0
  34. package/dist/session/link-types.d.ts.map +1 -0
  35. package/dist/session/links.d.ts +14 -0
  36. package/dist/session/links.d.ts.map +1 -0
  37. package/dist/session/model/index.d.ts +7 -0
  38. package/dist/session/model/index.d.ts.map +1 -0
  39. package/dist/session/model/index.js +25 -0
  40. package/dist/session/model/index.js.map +9 -0
  41. package/dist/session/model/paths.d.ts +24 -0
  42. package/dist/session/model/paths.d.ts.map +1 -0
  43. package/dist/session/model/safe-next.d.ts +15 -0
  44. package/dist/session/model/safe-next.d.ts.map +1 -0
  45. package/dist/session/model/second-factor.d.ts +16 -0
  46. package/dist/session/model/second-factor.d.ts.map +1 -0
  47. package/dist/session/model/site-path.d.ts +10 -0
  48. package/dist/session/model/site-path.d.ts.map +1 -0
  49. package/dist/session/model/step-up-answers.d.ts +40 -0
  50. package/dist/session/model/step-up-answers.d.ts.map +1 -0
  51. package/dist/session/model/step-up-props.d.ts +21 -0
  52. package/dist/session/model/step-up-props.d.ts.map +1 -0
  53. package/dist/session/model/step-up.d.ts +48 -0
  54. package/dist/session/model/step-up.d.ts.map +1 -0
  55. package/dist/session/second-factor.d.ts +22 -0
  56. package/dist/session/second-factor.d.ts.map +1 -0
  57. package/dist/session/serialize.d.ts +34 -0
  58. package/dist/session/serialize.d.ts.map +1 -0
  59. package/dist/session/sign-in.d.ts +18 -0
  60. package/dist/session/sign-in.d.ts.map +1 -0
  61. package/dist/session/step-up-codes.d.ts +9 -0
  62. package/dist/session/step-up-codes.d.ts.map +1 -0
  63. package/dist/session/step-up-types.d.ts +69 -0
  64. package/dist/session/step-up-types.d.ts.map +1 -0
  65. package/dist/session/step-up.d.ts +17 -0
  66. package/dist/session/step-up.d.ts.map +1 -0
  67. package/dist/session/step.d.ts +20 -0
  68. package/dist/session/step.d.ts.map +1 -0
  69. package/dist/session/viewer.d.ts +34 -0
  70. package/dist/session/viewer.d.ts.map +1 -0
  71. package/docs/README.md +1 -0
  72. package/docs/guide/errors.md +19 -0
  73. package/docs/guide/session.md +1002 -0
  74. package/docs/roadmap.md +27 -0
  75. package/docs/troubleshooting.md +390 -1
  76. package/package.json +15 -2
@@ -0,0 +1,21 @@
1
+ import type { StepUpVia } from './second-factor';
2
+ import type { StepUpState } from './step-up';
3
+ import type { StepUpError } from './step-up-answers';
4
+ /**
5
+ * The props of `@nxgt/material`'s `VerificationCode` and `StepUpDialog` that
6
+ * the state decides. The state's `message` is not one: the dialog words its
7
+ * own texts from `error`, `attemptsLeft`, `locked` and `retryAfter`. The callbacks (`onSubmit`, `onResend`) are the app's.
8
+ */
9
+ export interface StepUpProps {
10
+ readonly via?: StepUpVia;
11
+ readonly attemptsLeft?: number;
12
+ readonly locked: boolean;
13
+ readonly error?: StepUpError;
14
+ /** The seconds to wait when `error` is `throttled` and the API said how long. */
15
+ readonly retryAfter?: number;
16
+ readonly pending: boolean;
17
+ readonly resent: boolean;
18
+ }
19
+ /** The state as the dialog's props: `{ ...stepUpProps(state), onSubmit, onResend }`. */
20
+ export declare function stepUpProps(state: StepUpState): StepUpProps;
21
+ //# sourceMappingURL=step-up-props.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-up-props.d.ts","sourceRoot":"","sources":["../../../src/session/model/step-up-props.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CAY3D"}
@@ -0,0 +1,48 @@
1
+ import type { StepUpVia } from './second-factor';
2
+ import type { StepUpConfirmAnswer, StepUpError, StepUpRequestAnswer } from './step-up-answers';
3
+ /** Where a step-up is. */
4
+ export type StepUpPhase = 'idle' | 'requesting' | 'code' | 'confirming' | 'done';
5
+ export interface StepUpState {
6
+ readonly phase: StepUpPhase;
7
+ /** How the challenge held (or the last one) is confirmed. */
8
+ readonly via?: StepUpVia | undefined;
9
+ /** The challenge held. A secret: never in a URL, a log or loader data. */
10
+ readonly challenge?: string | undefined;
11
+ /** The tries the last refused code left. */
12
+ readonly attemptsLeft?: number | undefined;
13
+ /** No try left on an app or recovery code: wait, asking again would fail too. */
14
+ readonly locked: boolean;
15
+ readonly error?: StepUpError | undefined;
16
+ /** The last refusal worded for the page, until the next step or the release. */
17
+ readonly message?: string | undefined;
18
+ /** The seconds a throttled request asks to wait before asking again. */
19
+ readonly retryAfter?: number | undefined;
20
+ /** A request or a code is in flight. */
21
+ readonly pending: boolean;
22
+ /** The last request replaced a challenge that was held. */
23
+ readonly resent: boolean;
24
+ }
25
+ export type StepUpAction = {
26
+ readonly type: 'start';
27
+ } | {
28
+ readonly type: 'requested';
29
+ readonly answer: StepUpRequestAnswer;
30
+ } | {
31
+ readonly type: 'submit';
32
+ } | {
33
+ readonly type: 'confirmed';
34
+ readonly answer: StepUpConfirmAnswer;
35
+ } | {
36
+ readonly type: 'release';
37
+ };
38
+ export declare const INITIAL_STEP_UP: StepUpState;
39
+ /**
40
+ * The step-up's transitions, pure. `start` (a request in flight) forgets the
41
+ * last error but keeps a challenge held, so a refused "Resend code" leaves
42
+ * the code form up; a refusal keeps its `message` (and a throttle its
43
+ * `retryAfter`) until the next step; a granted request replaces the challenge; a code accepted
44
+ * spends it; an expired or spent one leaves "ask again"; at 0 tries left a
45
+ * mailed code is `expired` and an app or recovery code is `locked`.
46
+ */
47
+ export declare function stepUpReducer(state: StepUpState, action: StepUpAction): StepUpState;
48
+ //# sourceMappingURL=step-up.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-up.d.ts","sourceRoot":"","sources":["../../../src/session/model/step-up.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EACX,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,MAAM,mBAAmB,CAAC;AAE3B,0BAA0B;AAC1B,MAAM,MAAM,WAAW,GACpB,MAAM,GACN,YAAY,GACZ,MAAM,GACN,YAAY,GACZ,MAAM,CAAC;AAEV,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,4CAA4C;IAC5C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACzC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,wCAAwC;IACxC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEhC,eAAO,MAAM,eAAe,EAAE,WAK7B,CAAC;AA2DF;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,YAAY,GAClB,WAAW,CAoCb"}
@@ -0,0 +1,22 @@
1
+ import type { FlowContext, SignInAnswer, SignInFlow } from './flow-types';
2
+ import type { SecondFactorKind } from './model';
3
+ /**
4
+ * The code a second-factor page posted, with how it is given
5
+ * (`{ kind: 'code' | 'recovery', code }`, the code at most 40 characters). A
6
+ * native form post goes back to its page, which then posts JSON; a
7
+ * cross-site post is a 403; a body of another shape is a 400.
8
+ */
9
+ export declare function readCodeInput<L extends string, C>({ options }: FlowContext<L, C>, request: Request): Promise<{
10
+ kind: SecondFactorKind;
11
+ code: string;
12
+ }>;
13
+ /**
14
+ * The code against the sign-in's challenge: an issued code (an app's, or
15
+ * the one mailed) is confirmed, a recovery code is recovered. Either
16
+ * answers like a sign-in.
17
+ */
18
+ export declare function codeStep(input: {
19
+ readonly kind: SecondFactorKind;
20
+ readonly code: string;
21
+ }, challenge: string, steps: Parameters<SignInFlow<string>['codeStep']>[2]): Promise<SignInAnswer>;
22
+ //# sourceMappingURL=second-factor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"second-factor.d.ts","sourceRoot":"","sources":["../../src/session/second-factor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAQhD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACtD,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAC9B,OAAO,EAAE,OAAO,GACd,OAAO,CAAC;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACvB,KAAK,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjE,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,GAClD,OAAO,CAAC,YAAY,CAAC,CAIvB"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The bytes of a cookie, and the reading of one: `Name=v; Max-Age=n; Path=/;
3
+ * HttpOnly[; Secure]; SameSite=Lax`, in this order, so a session set by an
4
+ * app before it adopted this package is still read after, and the other way
5
+ * round.
6
+ */
7
+ export interface SerializeOptions {
8
+ /** Seconds; floored, and never below 0 (0 removes the cookie, and so does a value that is not finite). */
9
+ readonly maxAge: number;
10
+ /** `/` by default. */
11
+ readonly path?: string | undefined;
12
+ /** Adds `Secure`. Required: it is never read from the environment. */
13
+ readonly secure: boolean;
14
+ }
15
+ /** Whether `name` can name a cookie (an RFC 6265 token). */
16
+ export declare const isCookieName: (name: string) => boolean;
17
+ /** Whether `path` can be a cookie's `Path`: absolute, no `;`, no control character. */
18
+ export declare const isCookiePath: (path: string) => boolean;
19
+ /**
20
+ * A `Set-Cookie` value: always `HttpOnly` and `SameSite=Lax`, `Secure` when
21
+ * asked. A name or a path that could open another attribute throws a
22
+ * `TypeError`; a `maxAge` that is not finite is 0.
23
+ */
24
+ export declare function serializeCookie(name: string, value: string, { maxAge, path, secure }: SerializeOptions): string;
25
+ /**
26
+ * A cookie of the request: the first one of that name that is not empty,
27
+ * decoded; `null` when there is none, or when it is not valid percent-encoding.
28
+ */
29
+ export declare function readCookie(request: Request, name: string): string | null;
30
+ /** An instant as milliseconds; 0 when it is not a date. */
31
+ export declare function millisOf(at: string | Date): number;
32
+ /** Seconds from `now` until `expiresAt`: 0 when past or unreadable. */
33
+ export declare function secondsUntil(expiresAt: string | Date, now?: number): number;
34
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/session/serialize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAChC,0GAA0G;IAC1G,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sBAAsB;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB;AAQD,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,OAA2B,CAAC;AACxE,uFAAuF;AACvF,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,OAA0B,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,EAAE,MAAM,EAAE,IAAU,EAAE,MAAM,EAAE,EAAE,gBAAgB,GAC9C,MAAM,CAmBR;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAaxE;AAED,2DAA2D;AAC3D,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGlD;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,GAAG,GAAE,MAAmB,GACtB,MAAM,CAGR"}
@@ -0,0 +1,18 @@
1
+ import type { SignInFlow, SignInFlowOptions } from './flow-types';
2
+ /**
3
+ * The server side of a BFF's sign-in, over the cookies, the app's
4
+ * `createApiErrors` and its `jsonActions`: where an answer lands, the form
5
+ * steps with their refusals, the second factor, the viewer and sign-out.
6
+ * Built once, beside the app's cookies; it speaks `@nxgt/janus`'s words
7
+ * (`signedIn`, `secondFactor`, `CODE_INVALID`) and the app maps its API's.
8
+ *
9
+ * @example
10
+ * export const signIn = signInFlow({
11
+ * cookies, errors, json,
12
+ * revoke: (token, request, caller?: Caller) =>
13
+ * execute(SignOutMutation, {}, { token, ...caller }),
14
+ * attemptsLeft: (count, language) => t('auth.attempts-left', { count }, language),
15
+ * });
16
+ */
17
+ export declare function signInFlow<L extends string, C = undefined>(options: SignInFlowOptions<L, C>): SignInFlow<L, C>;
18
+ //# sourceMappingURL=sign-in.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign-in.d.ts","sourceRoot":"","sources":["../../src/session/sign-in.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAqC/E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,SAAS,EACzD,OAAO,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC9B,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CA0BlB"}
@@ -0,0 +1,9 @@
1
+ /** `@nxgt/janus`'s code for a mutation that needs a recent step-up. */
2
+ export declare const STEP_UP_REQUIRED = "STEP_UP_REQUIRED";
3
+ /**
4
+ * The codes that end a step-up's challenge: unknown, spent, expired or stale,
5
+ * and a second factor that changed since the request (`SECOND_FACTOR_ACTIVE`,
6
+ * `SECOND_FACTOR_NOT_ENROLLED`). The user asks again.
7
+ */
8
+ export declare const STEP_UP_OVER: readonly ["TOKEN_UNKNOWN", "TOKEN_SPENT", "TOKEN_EXPIRED", "TOKEN_STALE", "SECOND_FACTOR_ACTIVE", "SECOND_FACTOR_NOT_ENROLLED"];
9
+ //# sourceMappingURL=step-up-codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-up-codes.d.ts","sourceRoot":"","sources":["../../src/session/step-up-codes.ts"],"names":[],"mappings":"AAEA,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,YAAY,iIAKf,CAAC"}
@@ -0,0 +1,69 @@
1
+ import type { data } from 'react-router';
2
+ import type { ApiErrorLike } from '../errors/api-error';
3
+ import type { SignInCodes, SignInFlow } from './flow-types';
4
+ import type { StepUpConfirmAnswer, StepUpRequestAnswer, StepUpVia } from './model';
5
+ /** The codes the step-up tells apart; `@nxgt/janus`'s by default. */
6
+ export interface StepUpCodes extends SignInCodes {
7
+ /** The code of a mutation that needs a step-up first (`'STEP_UP_REQUIRED'`). */
8
+ readonly stepUpRequired?: string | undefined;
9
+ /** The code of a mail the API holds back (`'MAIL_THROTTLED'`): `request` answers `error: 'throttled'`. */
10
+ readonly mailThrottled?: string | undefined;
11
+ }
12
+ export interface StepUpStepsOptions<L extends string> {
13
+ /**
14
+ * The app's sign-in flow: the step-up reads its `errors`, its `json` (a
15
+ * step runs only for a same-site JSON post) and its `attemptsLeft`.
16
+ */
17
+ readonly signIn: SignInFlow<L>;
18
+ /** A challenge that is over, worded ("ask for a new code"). */
19
+ spent(language: L): string;
20
+ /**
21
+ * What an app or recovery code with no try left adds to its message: the
22
+ * account is locked, wait (asking again would fail too).
23
+ */
24
+ readonly locked?: ((language: L) => string) | undefined;
25
+ /**
26
+ * The app's own words for a refusal that is not a code's (a mail
27
+ * throttle's wait, say), before `errors`' default. `undefined` keeps the
28
+ * default.
29
+ */
30
+ readonly wording?: ((error: ApiErrorLike, language: L) => string | undefined) | undefined;
31
+ readonly codes?: StepUpCodes | undefined;
32
+ }
33
+ /** An answer that stays on the page: the answer, with the API's status. */
34
+ export type StepUpStayed<T> = ReturnType<typeof data<T>>;
35
+ /**
36
+ * What each step takes beside `run`: the request, whose headers must be a
37
+ * same-site JSON post's (a native form post goes back to its page, another
38
+ * site is a 403), and the language of the wording.
39
+ */
40
+ export interface StepUpCall<L extends string> {
41
+ readonly request: Request;
42
+ readonly language: L;
43
+ }
44
+ export interface StepUpSteps<L extends string> {
45
+ /**
46
+ * Starts a step-up (`requestStepUp`): the challenge and how it is
47
+ * confirmed. The challenge goes to the page's fetcher and nowhere else.
48
+ * A refusal is `error: 'failed'` with the
49
+ * message and the API's status, and a throttled mail `error: 'throttled'`,
50
+ * with `retryAfter` when the API says how long; an API it cannot reach is a 503. Anything
51
+ * that is not an API failure is thrown. Only a same-site JSON post goes on.
52
+ */
53
+ request(run: () => Promise<{
54
+ readonly challenge: string;
55
+ readonly via: StepUpVia;
56
+ }>, options: StepUpCall<L>): Promise<StepUpRequestAnswer | StepUpStayed<StepUpRequestAnswer>>;
57
+ /**
58
+ * Confirms the step-up with the code (`confirmStepUp`). A refusal says
59
+ * why: `invalid` with the tries left, `expired` (ask again), or `failed`;
60
+ * with no try left, a mailed code (`via: 'email'`, or not given) is
61
+ * `expired` and an app or recovery code is `locked`.
62
+ */
63
+ confirm(run: () => Promise<unknown>, options: StepUpCall<L> & {
64
+ readonly via?: StepUpVia | undefined;
65
+ }): Promise<StepUpConfirmAnswer | StepUpStayed<StepUpConfirmAnswer>>;
66
+ /** Whether `error` is the API asking for a step-up first. */
67
+ isStepUpRequired(error: unknown): boolean;
68
+ }
69
+ //# sourceMappingURL=step-up-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-up-types.d.ts","sourceRoot":"","sources":["../../src/session/step-up-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EACX,mBAAmB,EACnB,mBAAmB,EACnB,SAAS,EACT,MAAM,SAAS,CAAC;AAEjB,qEAAqE;AACrE,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,gFAAgF;IAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,0GAA0G;IAC1G,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,MAAM;IACnD;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAC/B,+DAA+D;IAC/D,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EACd,CAAC,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC,GAC1D,SAAS,CAAC;IACb,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CACzC;AAED,2EAA2E;AAC3E,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,MAAM;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM;IAC5C;;;;;;;OAOG;IACH,OAAO,CACN,GAAG,EAAE,MAAM,OAAO,CAAC;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;KAAE,CAAC,EAC3E,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,mBAAmB,GAAG,YAAY,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACpE;;;;;OAKG;IACH,OAAO,CACN,GAAG,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAC3B,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;KAAE,GAC/D,OAAO,CAAC,mBAAmB,GAAG,YAAY,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACpE,6DAA6D;IAC7D,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;CAC1C"}
@@ -0,0 +1,17 @@
1
+ import type { StepUpSteps, StepUpStepsOptions } from './step-up-types';
2
+ /**
3
+ * The server side of a BFF's step-up: `request` and `confirm` answer the
4
+ * page's fetcher with `StepUpRequestAnswer` and `StepUpConfirmAnswer`, which
5
+ * `stepUpReducer` takes as they are. It speaks `@nxgt/janus`'s words
6
+ * (`CODE_INVALID`, `TOKEN_*`, `STEP_UP_REQUIRED`); the app maps the rest.
7
+ *
8
+ * @example
9
+ * const stepUp = stepUpSteps({
10
+ * errors,
11
+ * attemptsLeft: (count, language) => t('code.attempts-left', { count }, language),
12
+ * spent: (language) => t('step-up.spent', {}, language),
13
+ * locked: (language) => t('step-up.locked', {}, language),
14
+ * });
15
+ */
16
+ export declare function stepUpSteps<L extends string>(options: StepUpStepsOptions<L>): StepUpSteps<L>;
17
+ //# sourceMappingURL=step-up.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-up.d.ts","sourceRoot":"","sources":["../../src/session/step-up.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAEX,WAAW,EACX,kBAAkB,EAClB,MAAM,iBAAiB,CAAC;AA8IzB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC3C,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC5B,WAAW,CAAC,CAAC,CAAC,CAQhB"}
@@ -0,0 +1,20 @@
1
+ import { type ApiErrorLike } from '../errors/api-error';
2
+ import type { CodeWording, FlowContext, SignInAnswer, SignInFlow, SignInFlowOptions, SignInStayed } from './flow-types';
3
+ /**
4
+ * A refused code's message, with the tries left when the API counts them;
5
+ * `undefined` when the challenge is over (gone, or no try left and no
6
+ * `whenNone` to say instead) and the user must sign in again.
7
+ */
8
+ export declare function codeRefusal<L extends string>({ options, invalidCode, challengeOver }: CodeWording<L>, error: ApiErrorLike, language: L, whenNone?: string): string | undefined;
9
+ /** What a sign-in says when the API cannot be reached. */
10
+ export declare const unavailableMessage: <L extends string>({ unavailable, errors }: Pick<SignInFlowOptions<L>, "unavailable" | "errors">, language: L) => string;
11
+ /**
12
+ * Runs a sign-in step and redirects where its answer leads, a new session
13
+ * replacing the browser's (`landReplacing`). A refusal is the form's
14
+ * message (a wrong password, a lock with its delay); an unreachable API
15
+ * says `unavailable`, with a 503.
16
+ */
17
+ export declare function signInStep<L extends string, C, T extends object>(flow: FlowContext<L, C>, run: () => Promise<SignInAnswer>, { request, language, next, extra, refused, context, }: Parameters<SignInFlow<L, C>['signInStep']>[1] & {
18
+ extra: T;
19
+ }): Promise<Response | SignInStayed<T>>;
20
+ //# sourceMappingURL=step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step.d.ts","sourceRoot":"","sources":["../../src/session/step.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,YAAY,EAGjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACX,WAAW,EACX,WAAW,EACX,YAAY,EACZ,UAAU,EACV,iBAAiB,EAEjB,YAAY,EACZ,MAAM,cAAc,CAAC;AAItB;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC3C,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EACvD,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,CAAC,EACX,QAAQ,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAapB;AAED,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,MAAM,EAClD,yBAAyB,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,aAAa,GAAG,QAAQ,CAAC,EAC7E,UAAU,CAAC,KACT,MAAwE,CAAC;AAE5E;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM,EACrE,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EACvB,GAAG,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,EAChC,EACC,OAAO,EACP,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,GACP,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG;IAAE,KAAK,EAAE,CAAC,CAAA;CAAE,GAC7D,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAwBrC"}
@@ -0,0 +1,34 @@
1
+ import type { FlowContext } from './flow-types';
2
+ /** The `next` a sign-in page was opened with, safe to redirect to. */
3
+ export declare const nextParam: (request: Request) => string;
4
+ /**
5
+ * The sign-in page, coming back to this request's page afterwards, with the
6
+ * session cookie cleared: what a page answers without a (live) session. A
7
+ * request that carried a session cookie says it ended when `endedParam` is
8
+ * set.
9
+ */
10
+ export declare function signInRedirect<L extends string, C>({ options, paths }: FlowContext<L, C>, request: Request): Response;
11
+ /**
12
+ * For the sign-in pages: the viewer when the cookie holds a live session,
13
+ * `null` when the API refuses it (a 401), `undefined` without a cookie or
14
+ * when the API cannot tell.
15
+ */
16
+ export declare function viewer<L extends string, C, V>({ options }: FlowContext<L, C>, request: Request, read: (token: string) => Promise<V | null>, token?: string | null | undefined): Promise<V | null | undefined>;
17
+ /**
18
+ * The signed-in viewer, else the sign-in redirect. Its `headers` move the
19
+ * cookie's expiry to the session's when the API moved it.
20
+ */
21
+ export declare function requireViewer<L extends string, C, V>(context: FlowContext<L, C>, request: Request, read: (token: string) => Promise<V | null>, expiresAt: (viewer: V) => string | null): Promise<{
22
+ viewer: V;
23
+ headers: HeadersInit;
24
+ }>;
25
+ /**
26
+ * Ends the session and goes to the sign-in page (`?signed-out=1`), or to
27
+ * `to`. The body is read as JSON first: JSON and same-origin only, so
28
+ * another site cannot sign the user out.
29
+ */
30
+ export declare function signOut<L extends string, C>(flow: FlowContext<L, C>, request: Request, { to, context, }?: {
31
+ readonly to?: string | undefined;
32
+ readonly context?: C | undefined;
33
+ }): Promise<Response>;
34
+ //# sourceMappingURL=viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer.d.ts","sourceRoot":"","sources":["../../src/session/viewer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,sEAAsE;AACtE,eAAO,MAAM,SAAS,GAAI,SAAS,OAAO,KAAG,MACW,CAAC;AAEzD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACjD,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EACrC,OAAO,EAAE,OAAO,GACd,QAAQ,CAWV;AAED;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAClD,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAC9B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAC1C,KAAK,GAAE,MAAM,GAAG,IAAI,GAAG,SAA0C,GAC/D,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAO/B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EACzD,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAC1C,SAAS,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GACrC,OAAO,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC,CAiB9C;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAChD,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EACvB,OAAO,EAAE,OAAO,EAChB,EACC,EAAE,EACF,OAAO,GACP,GAAE;IACF,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC;CAC5B,GACJ,OAAO,CAAC,QAAQ,CAAC,CAcnB"}
package/docs/README.md CHANGED
@@ -19,6 +19,7 @@ installable and usable offline, wording an API's refusals, and what to do with e
19
19
  | [Deploying](guide/deploying.md) | running the build with `bun build/server/index.js`, and the Docker image |
20
20
  | [Installable and offline](guide/pwa.md) | making the app installable and usable offline: the subpaths of `@alxia/react-router/pwa`, the service worker (`startWorker`, `/pwa/worker`), its build by `alxia()` or `buildWorker` (`/pwa/build`), its contract and cache names, which requests the worker caches and which responses it stores, the precache list read from a build, or clearing the caches at sign-out |
21
21
  | [An API's refusals](guide/errors.md) | wording a GraphQL API's refusals for the pages: recognising an `ApiError`, the request id, the page a request is for, the catalogue store, the form checks in the API's words, the provider, and the loaders, the error page and the actions that answer with it, the JSON actions with their CSRF check and fetcher hook, calling the API from a loader (`apiCallFailure`), and keeping the catalogue fresh (`@alxia/react-router/errors`) |
22
+ | [A BFF session](guide/session.md) | redirecting after sign-in to a page of the site and nowhere else, the sign-in pages' paths, the session's cookies, the sign-in flow (where an answer lands, a refused code, the viewer, sign-out; `@alxia/react-router/session`), the pages a mail's link opens (a magic link, a password reset: `authLinks`), and the step-up's state and the props it gives the UI (`@alxia/react-router/session/model`) |
22
23
  | [Troubleshooting](troubleshooting.md) | something went wrong and you have the message, or a page streams, hydrates or reads its context unlike you expected |
23
24
  | [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
24
25
 
@@ -29,6 +29,7 @@ import is `react-router`, never `@alxia/core`, so a page can import it
29
29
  | `ApiErrorLike`, `isApiErrorLike(error)` | the shape of `@nxgt/graphql-client`'s `ApiError`, and the test for it |
30
30
  | `ApiErrorEntryLike`, `ApiErrorExtensionsLike`, `ApiErrorParamsLike`, `ApiFieldErrorLike` | one error of the response, its `extensions`, their `params`, and one refused field |
31
31
  | `waitMinutes(extensions)` | the minutes of a throttle's wait: `minutes` (at least one), else `retryAfter` rounded up to minutes |
32
+ | `attemptsLeftOf(error)` | the tries a refused code leaves, as `@nxgt/janus` counts them: `extensions.attemptsLeft` of the first error, else `params.attemptsLeft`; `undefined` when it is not a whole number from 0 up |
32
33
  | `REQUEST_ID_HEADER`, `requestIdOf(response)` | the header an API echoes on every response, and its value |
33
34
  | `nextOf(url)` | the page a request is for, as `next` carries it back after sign-in |
34
35
  | `FormErrors` | what a form action answers when the API refused the input |
@@ -73,6 +74,24 @@ waitMinutes({ retryAfter: 90 }); // 2
73
74
  waitMinutes({ params: { minutes: 4 } }); // 4
74
75
  ```
75
76
 
77
+ ### The tries a code leaves
78
+
79
+ A wrong second-factor or step-up code is refused with `@nxgt/janus`'s
80
+ `CODE_INVALID` and the tries left in `extensions.attemptsLeft`.
81
+ `attemptsLeftOf(error)` reads that count from the first error, else from its
82
+ `params`, and answers `undefined` for anything that is not a whole number from
83
+ 0 up (a string, a negative number, a missing key). `0` is a count: no try is
84
+ left.
85
+
86
+ ```ts
87
+ import { attemptsLeftOf } from '@alxia/react-router/errors';
88
+
89
+ attemptsLeftOf(error); // 3 after a wrong code, 0 on the last, undefined if uncounted
90
+ ```
91
+
92
+ [`signInFlow`'s `codeRefusal`](session.md#the-second-factor) words it for a
93
+ sign-in's code page.
94
+
76
95
  ## The request id
77
96
 
78
97
  An API that echoes `x-request-id` lets a page tell the user which request