@alxia/react-router 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -1
- package/dist/chunks/index-0tyw54ad.js +160 -0
- package/dist/chunks/index-0tyw54ad.js.map +15 -0
- package/dist/chunks/index-c3k47yw7.js +124 -0
- package/dist/chunks/index-c3k47yw7.js.map +14 -0
- package/dist/errors/api-error.d.ts +18 -0
- package/dist/errors/api-error.d.ts.map +1 -1
- package/dist/errors/api-errors.d.ts +16 -0
- package/dist/errors/api-errors.d.ts.map +1 -1
- package/dist/errors/describe.d.ts +1 -1
- package/dist/errors/describe.d.ts.map +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +51 -113
- package/dist/errors/index.js.map +6 -10
- package/dist/errors/json-actions.d.ts.map +1 -1
- package/dist/errors/same-site.d.ts +25 -0
- package/dist/errors/same-site.d.ts.map +1 -0
- package/dist/session/codes.d.ts +7 -0
- package/dist/session/codes.d.ts.map +1 -0
- package/dist/session/cookies.d.ts +60 -0
- package/dist/session/cookies.d.ts.map +1 -0
- package/dist/session/flow-types.d.ts +178 -0
- package/dist/session/flow-types.d.ts.map +1 -0
- package/dist/session/guard.d.ts +10 -0
- package/dist/session/guard.d.ts.map +1 -0
- package/dist/session/index.d.ts +18 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +637 -0
- package/dist/session/index.js.map +23 -0
- package/dist/session/land.d.ts +26 -0
- package/dist/session/land.d.ts.map +1 -0
- package/dist/session/link-land.d.ts +18 -0
- package/dist/session/link-land.d.ts.map +1 -0
- package/dist/session/link-token.d.ts +22 -0
- package/dist/session/link-token.d.ts.map +1 -0
- package/dist/session/link-types.d.ts +124 -0
- package/dist/session/link-types.d.ts.map +1 -0
- package/dist/session/links.d.ts +14 -0
- package/dist/session/links.d.ts.map +1 -0
- package/dist/session/model/index.d.ts +7 -0
- package/dist/session/model/index.d.ts.map +1 -0
- package/dist/session/model/index.js +25 -0
- package/dist/session/model/index.js.map +9 -0
- package/dist/session/model/paths.d.ts +24 -0
- package/dist/session/model/paths.d.ts.map +1 -0
- package/dist/session/model/safe-next.d.ts +15 -0
- package/dist/session/model/safe-next.d.ts.map +1 -0
- package/dist/session/model/second-factor.d.ts +16 -0
- package/dist/session/model/second-factor.d.ts.map +1 -0
- package/dist/session/model/site-path.d.ts +10 -0
- package/dist/session/model/site-path.d.ts.map +1 -0
- package/dist/session/model/step-up-answers.d.ts +40 -0
- package/dist/session/model/step-up-answers.d.ts.map +1 -0
- package/dist/session/model/step-up-props.d.ts +21 -0
- package/dist/session/model/step-up-props.d.ts.map +1 -0
- package/dist/session/model/step-up.d.ts +48 -0
- package/dist/session/model/step-up.d.ts.map +1 -0
- package/dist/session/second-factor.d.ts +22 -0
- package/dist/session/second-factor.d.ts.map +1 -0
- package/dist/session/serialize.d.ts +34 -0
- package/dist/session/serialize.d.ts.map +1 -0
- package/dist/session/sign-in.d.ts +17 -0
- package/dist/session/sign-in.d.ts.map +1 -0
- package/dist/session/step-up-codes.d.ts +9 -0
- package/dist/session/step-up-codes.d.ts.map +1 -0
- package/dist/session/step-up-types.d.ts +69 -0
- package/dist/session/step-up-types.d.ts.map +1 -0
- package/dist/session/step-up.d.ts +17 -0
- package/dist/session/step-up.d.ts.map +1 -0
- package/dist/session/step.d.ts +20 -0
- package/dist/session/step.d.ts.map +1 -0
- package/dist/session/viewer.d.ts +33 -0
- package/dist/session/viewer.d.ts.map +1 -0
- package/docs/README.md +1 -0
- package/docs/guide/errors.md +45 -0
- package/docs/guide/session.md +955 -0
- package/docs/roadmap.md +27 -0
- package/docs/troubleshooting.md +362 -1
- package/package.json +15 -2
|
@@ -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>({ options }: FlowContext<L>, 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,EACnD,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAC3B,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,17 @@
|
|
|
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) => execute(SignOutMutation, {}, { token }),
|
|
13
|
+
* attemptsLeft: (count, language) => t('auth.attempts-left', { count }, language),
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
export declare function signInFlow<L extends string>(options: SignInFlowOptions<L>): SignInFlow<L>;
|
|
17
|
+
//# 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;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAC1C,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC3B,UAAU,CAAC,CAAC,CAAC,CA0Bf"}
|
|
@@ -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, T extends object>(context: FlowContext<L>, run: () => Promise<SignInAnswer>, { request, language, next, extra, refused, }: Parameters<SignInFlow<L>['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,SAAS,MAAM,EAClE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EACvB,GAAG,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,EAChC,EACC,OAAO,EACP,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,OAAO,GACP,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG;IAAE,KAAK,EAAE,CAAC,CAAA;CAAE,GAC1D,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAuBrC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
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>({ options, paths }: FlowContext<L>, 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, V>({ options }: FlowContext<L>, 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, V>(context: FlowContext<L>, 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>(context: FlowContext<L>, request: Request, { to }?: {
|
|
31
|
+
readonly to?: string | undefined;
|
|
32
|
+
}): Promise<Response>;
|
|
33
|
+
//# 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,EAC9C,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAClC,OAAO,EAAE,OAAO,GACd,QAAQ,CAWV;AAED;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAC/C,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAC3B,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,EACtD,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EACvB,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,EAC7C,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EACvB,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,EAAE,GAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GAC/C,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
|
|
package/docs/guide/errors.md
CHANGED
|
@@ -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
|
|
@@ -323,6 +342,32 @@ catalogue, then the API's own message. `fieldErrors` uses the same order for
|
|
|
323
342
|
a single `extensions.field`, while the entries of `extensions.fields` go
|
|
324
343
|
straight to the catalogue.
|
|
325
344
|
|
|
345
|
+
`reword` words a whole family of codes once, after the text is worded, so
|
|
346
|
+
there is no list of codes to keep in step with the catalogue. It receives the
|
|
347
|
+
final text, the error, and `{ code, values, language }`: `values` are the
|
|
348
|
+
merged values the catalogue formats with (`extensions` and
|
|
349
|
+
`extensions.params`, the params winning). Return the text to show, or
|
|
350
|
+
`undefined` to keep it:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
export const apiErrors = createApiErrors<'en' | 'fr'>({
|
|
354
|
+
messages: apiMessages,
|
|
355
|
+
unavailable: (language) => translate('app.error.unavailable', language),
|
|
356
|
+
reword: (text, error, { code }) =>
|
|
357
|
+
code.endsWith('.errors.referenced')
|
|
358
|
+
? `${text} ${stillLinkedTo(error)}`
|
|
359
|
+
: undefined,
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`reword` sees the text `apiErrorMessage` returns for an error with a code,
|
|
364
|
+
whichever produced it: the catalogue, the API's own message, or `byCode`; so
|
|
365
|
+
`apiRefusal`, `fetcherResult`, `fetcherRefusal`, `submitForm` and a single
|
|
366
|
+
`extensions.field` are reworded too. It does not see `refused`'s text (an
|
|
367
|
+
error with no API code), `unavailable`'s (a network failure, which is no API
|
|
368
|
+
code), `translateCode`'s, nor the entries of `extensions.fields`, which go
|
|
369
|
+
straight to the catalogue.
|
|
370
|
+
|
|
326
371
|
`isApiCode` changes what counts as an API code (`<module>.errors.<slug>` by
|
|
327
372
|
default). Here only the codes of the `todos` module are the catalogue's; any
|
|
328
373
|
other code, even one shaped like a catalogue code, says `refused`:
|