@alxia/react-router 0.13.0 → 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 +78 -0
- 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/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +45 -110
- package/dist/errors/index.js.map +4 -8
- 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 +19 -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,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/session/codes.ts", "../src/session/serialize.ts", "../src/session/cookies.ts", "../src/session/link-token.ts", "../src/session/guard.ts", "../src/session/links.ts", "../src/session/link-land.ts", "../src/session/land.ts", "../src/session/second-factor.ts", "../src/session/step.ts", "../src/session/viewer.ts", "../src/session/sign-in.ts", "../src/session/step-up.ts", "../src/session/step-up-codes.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/** `@nxgt/janus`'s code for a wrong second-factor or step-up code. */\nexport const INVALID_CODE = 'CODE_INVALID';\n\n/** `@nxgt/janus`'s codes for a challenge that is gone: unknown, spent or expired. */\nexport const CHALLENGE_OVER = [\n\t'TOKEN_UNKNOWN',\n\t'TOKEN_SPENT',\n\t'TOKEN_EXPIRED',\n] as const;\n\n/** `@nxgt/janus`'s code for a mail the API holds back: a link or a code asked for too often. */\nexport const MAIL_THROTTLED = 'MAIL_THROTTLED';\n",
|
|
6
|
+
"/**\n * The bytes of a cookie, and the reading of one: `Name=v; Max-Age=n; Path=/;\n * HttpOnly[; Secure]; SameSite=Lax`, in this order, so a session set by an\n * app before it adopted this package is still read after, and the other way\n * round.\n */\n\nexport interface SerializeOptions {\n\t/** Seconds; floored, and never below 0 (0 removes the cookie, and so does a value that is not finite). */\n\treadonly maxAge: number;\n\t/** `/` by default. */\n\treadonly path?: string | undefined;\n\t/** Adds `Secure`. Required: it is never read from the environment. */\n\treadonly secure: boolean;\n}\n\n// RFC 6265 `token`: what a cookie name may be made of.\nconst TOKEN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n// A path of the site: no `;` to open another attribute, no control character.\n// biome-ignore lint/suspicious/noControlCharactersInRegex: that is the point\nconst PATH = /^\\/[^;\\x00-\\x1f\\x7f]*$/;\n\n/** Whether `name` can name a cookie (an RFC 6265 token). */\nexport const isCookieName = (name: string): boolean => TOKEN.test(name);\n/** Whether `path` can be a cookie's `Path`: absolute, no `;`, no control character. */\nexport const isCookiePath = (path: string): boolean => PATH.test(path);\n\n/**\n * A `Set-Cookie` value: always `HttpOnly` and `SameSite=Lax`, `Secure` when\n * asked. A name or a path that could open another attribute throws a\n * `TypeError`; a `maxAge` that is not finite is 0.\n */\nexport function serializeCookie(\n\tname: string,\n\tvalue: string,\n\t{ maxAge, path = '/', secure }: SerializeOptions,\n): string {\n\tif (!isCookieName(name)) {\n\t\tthrow new TypeError(\n\t\t\t`serializeCookie(): the name ${JSON.stringify(name)} is not a cookie name (an RFC 6265 token).`,\n\t\t);\n\t}\n\tif (!isCookiePath(path)) {\n\t\tthrow new TypeError(\n\t\t\t`serializeCookie(): the path ${JSON.stringify(path)} is not a cookie path: it must start with \"/\" and hold no \";\" or control character.`,\n\t\t);\n\t}\n\treturn [\n\t\t`${name}=${encodeURIComponent(value)}`,\n\t\t`Max-Age=${Number.isFinite(maxAge) ? Math.max(0, Math.floor(maxAge)) : 0}`,\n\t\t`Path=${path}`,\n\t\t'HttpOnly',\n\t\t...(secure ? ['Secure'] : []),\n\t\t'SameSite=Lax',\n\t].join('; ');\n}\n\n/**\n * A cookie of the request: the first one of that name that is not empty,\n * decoded; `null` when there is none, or when it is not valid percent-encoding.\n */\nexport function readCookie(request: Request, name: string): string | null {\n\tfor (const pair of request.headers.get('cookie')?.split(';') ?? []) {\n\t\tconst index = pair.indexOf('=');\n\t\tif (index < 0 || pair.slice(0, index).trim() !== name) continue;\n\t\tconst raw = pair.slice(index + 1).trim();\n\t\tif (!raw) continue;\n\t\ttry {\n\t\t\treturn decodeURIComponent(raw);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** An instant as milliseconds; 0 when it is not a date. */\nexport function millisOf(at: string | Date): number {\n\tconst time = typeof at === 'string' ? Date.parse(at) : at.getTime();\n\treturn Number.isNaN(time) ? 0 : time;\n}\n\n/** Seconds from `now` until `expiresAt`: 0 when past or unreadable. */\nexport function secondsUntil(\n\texpiresAt: string | Date,\n\tnow: number = Date.now(),\n): number {\n\tconst end = millisOf(expiresAt);\n\treturn end === 0 ? 0 : Math.max(0, (end - now) / 1000);\n}\n",
|
|
7
|
+
"import {\n\tisCookieName,\n\tisCookiePath,\n\tmillisOf,\n\treadCookie,\n\tsecondsUntil,\n\tserializeCookie,\n} from './serialize';\n\nexport interface SessionCookiesOptions {\n\t/**\n\t * The base name of the session cookie, e.g. `'my-app_session'`. With\n\t * `secure`, the cookie is named `__Host-my-app_session`.\n\t */\n\treadonly session: string;\n\t/**\n\t * The base name of the second-factor challenge's cookie. With `secure`, it\n\t * is named `__Secure-<name>`.\n\t */\n\treadonly challenge: string;\n\t/**\n\t * The app is served over HTTPS. Required, and never read from the\n\t * environment: pass `!import.meta.env.DEV`. `false` drops `Secure` and the\n\t * prefixes (Safari drops a `Secure` cookie over `http://localhost`).\n\t */\n\treadonly secure: boolean;\n\t/** Where the browser sends the challenge. `'/sign-in'` by default. */\n\treadonly challengePath?: string | undefined;\n\t/** The most the challenge lives. 300, `@nxgt/janus`'s lifetime, by default. */\n\treadonly challengeSeconds?: number | undefined;\n}\n\n/** What the session cookie holds: the token, and the expiry (ms) it was last set to. */\nexport interface CookieSession {\n\treadonly token: string;\n\treadonly expiresAt: number;\n}\n\nexport interface SessionCookies {\n\t/** The names as sent, prefixes included. */\n\treadonly names: { readonly session: string; readonly challenge: string };\n\t/** The session cookie of the request; `null` when absent or not `<ms>.<token>`. */\n\tread(request: Request): CookieSession | null;\n\t/** The session token of the request, or `null`. */\n\ttoken(request: Request): string | null;\n\t/** A `Set-Cookie` value keeping `token` until `expiresAt`. */\n\tcommit(token: string, expiresAt: string | Date, now?: number): string;\n\t/** A `Set-Cookie` value removing the session cookie. */\n\tdestroy(): string;\n\t/** The `Set-Cookie` header that keeps the cookie as long as the session lives; empty when the expiry did not move, or is absent or not a date. */\n\trefreshed(\n\t\tsession: CookieSession,\n\t\texpiresAt: string | Date | null,\n\t): HeadersInit;\n\t/** The challenge cookie of the request, or `null`. */\n\tchallengeOf(request: Request): string | null;\n\t/** A `Set-Cookie` value keeping the challenge for the code page. */\n\tcommitChallenge(\n\t\tchallenge: string,\n\t\texpiresAt?: string | Date | null,\n\t\tnow?: number,\n\t): string;\n\t/** A `Set-Cookie` value removing the challenge. */\n\tdestroyChallenge(): string;\n\t/**\n\t * What a signed-out visit of the sign-in page sets: the challenge cleared\n\t * blind (a client navigation fetches `/sign-in.data`, which the cookie's\n\t * path does not match, so the request never shows it), and a dead\n\t * session's cookie when `deadSession`.\n\t */\n\tsignInPageHeaders(deadSession: boolean): Headers;\n}\n\nconst PREFIX = /^__(?:Host|Secure)-/;\n\nfunction checkName(label: string, name: string): void {\n\tif (!isCookieName(name)) {\n\t\tthrow new TypeError(\n\t\t\t`sessionCookies(): the ${label} name ${JSON.stringify(name)} is not a cookie name (an RFC 6265 token: letters, digits and ! # $ % & ' * + - . ^ _ \\` | ~). Pass a name such as \"my-app_${label}\".`,\n\t\t);\n\t}\n\tif (PREFIX.test(name)) {\n\t\tthrow new TypeError(\n\t\t\t`sessionCookies(): the ${label} name ${JSON.stringify(name)} already carries a prefix. Pass the base name: the package adds __Host- (session) or __Secure- (challenge) when secure is true.`,\n\t\t);\n\t}\n}\n\n/** The cookies of a BFF's session and of its second-factor challenge. */\nexport function sessionCookies(options: SessionCookiesOptions): SessionCookies {\n\tconst {\n\t\tsecure,\n\t\tchallengePath = '/sign-in',\n\t\tchallengeSeconds = 5 * 60,\n\t} = options;\n\tcheckName('session', options.session);\n\tcheckName('challenge', options.challenge);\n\tif (!Number.isInteger(challengeSeconds) || challengeSeconds <= 0) {\n\t\tthrow new TypeError(\n\t\t\t`sessionCookies(): challengeSeconds must be a positive integer; got ${String(challengeSeconds)}.`,\n\t\t);\n\t}\n\tif (!isCookiePath(challengePath)) {\n\t\tthrow new TypeError(\n\t\t\t`sessionCookies(): challengePath must start with \"/\" and hold no \";\" or control character; got ${JSON.stringify(challengePath)}.`,\n\t\t);\n\t}\n\t// `__Host-` holds only with `Path=/` and no `Domain`: the session cookie\n\t// sets neither option.\n\tconst session = secure ? `__Host-${options.session}` : options.session;\n\tconst challenge = secure\n\t\t? `__Secure-${options.challenge}`\n\t\t: options.challenge;\n\tconst clear = (name: string, path?: string) =>\n\t\tserializeCookie(name, '', { maxAge: 0, secure, ...(path && { path }) });\n\n\tconst read = (request: Request): CookieSession | null => {\n\t\tconst value = readCookie(request, session);\n\t\tconst dot = value?.indexOf('.') ?? -1;\n\t\tif (!value || dot < 1) return null;\n\t\tconst digits = value.slice(0, dot);\n\t\tconst expiresAt = Number(digits);\n\t\tconst token = value.slice(dot + 1);\n\t\treturn token && /^\\d+$/.test(digits) && Number.isSafeInteger(expiresAt)\n\t\t\t? { token, expiresAt }\n\t\t\t: null;\n\t};\n\t// The value is `<expiry ms>.<token>`: the expiry only tells a later request\n\t// whether the API moved it, so the cookie is set again then. It is never\n\t// trusted: the API alone decides whether a session is valid.\n\tconst commit = (token: string, expiresAt: string | Date, now?: number) =>\n\t\tserializeCookie(session, `${millisOf(expiresAt)}.${token}`, {\n\t\t\tmaxAge: secondsUntil(expiresAt, now),\n\t\t\tsecure,\n\t\t});\n\n\treturn {\n\t\tnames: { session, challenge },\n\t\tread,\n\t\ttoken: (request) => read(request)?.token ?? null,\n\t\tcommit,\n\t\tdestroy: () => clear(session),\n\t\trefreshed(current, expiresAt) {\n\t\t\t// An expiry that is not a date is garbage, not an end: keep the cookie.\n\t\t\tconst end = expiresAt ? millisOf(expiresAt) : 0;\n\t\t\tif (!expiresAt || end === 0 || end === current.expiresAt) return {};\n\t\t\treturn { 'Set-Cookie': commit(current.token, expiresAt) };\n\t\t},\n\t\tchallengeOf: (request) => readCookie(request, challenge),\n\t\tcommitChallenge: (value, expiresAt, now) =>\n\t\t\tserializeCookie(challenge, value, {\n\t\t\t\tmaxAge: Math.min(\n\t\t\t\t\tchallengeSeconds,\n\t\t\t\t\texpiresAt ? secondsUntil(expiresAt, now) : challengeSeconds,\n\t\t\t\t),\n\t\t\t\tpath: challengePath,\n\t\t\t\tsecure,\n\t\t\t}),\n\t\tdestroyChallenge: () => clear(challenge, challengePath),\n\t\tsignInPageHeaders(deadSession) {\n\t\t\tconst headers = new Headers();\n\t\t\theaders.append('Set-Cookie', clear(challenge, challengePath));\n\t\t\tif (deadSession) headers.append('Set-Cookie', clear(session));\n\t\t\treturn headers;\n\t\t},\n\t};\n}\n",
|
|
8
|
+
"import { data, replace } from 'react-router';\nimport { isApiErrorLike } from '../errors/api-error';\nimport type { FormErrors } from '../errors/form-errors';\nimport type { InferOutput, StandardSchemaV1 } from '../errors/standard-schema';\nimport { samesitePost } from './guard';\nimport type {\n\tLinkStayed,\n\tLinksContext,\n\tSpendOptions,\n\tSpent,\n} from './link-types';\nimport { sitePath } from './model/site-path';\n\n/**\n * The response headers of a page a mail's link opens, whose URL carries a\n * token: no `Referer` sends it to another site (its fonts included), and\n * no search engine keeps it.\n */\nexport const TOKEN_PAGE_HEADERS = {\n\t'Referrer-Policy': 'no-referrer',\n\t'X-Robots-Tag': 'noindex, nofollow',\n} as const;\n\n/** A token page's path with no query or fragment, or a `TypeError` naming `call`. */\nconst pathOf = (path: string, call: string): string =>\n\tsitePath(path, { label: `${call}: a token page's path` });\n\nexport const statePath = (path: string, state: string): string =>\n\t`${pathOf(path, 'statePath()')}?${new URLSearchParams({ state })}`;\n\n/** `headers`, with the token page's own on top. */\nexport function withTokenHeaders(headers?: HeadersInit): Headers {\n\tconst merged = new Headers(headers);\n\tfor (const [name, value] of Object.entries(TOKEN_PAGE_HEADERS)) {\n\t\tmerged.set(name, value);\n\t}\n\treturn merged;\n}\n\n/** A redirect replacing the current history entry: the token's, so Back never opens it again. */\nexport const stateRedirect = (\n\tpath: string,\n\tstate: string,\n\theaders?: HeadersInit,\n): Response =>\n\treplace(statePath(path, state), { headers: withTokenHeaders(headers) });\n\nconst isTokenIssue = (issue: {\n\treadonly path?:\n\t\t| readonly (PropertyKey | { readonly key: PropertyKey })[]\n\t\t| undefined;\n}) => {\n\tconst first = issue.path?.[0];\n\treturn (typeof first === 'object' ? first.key : first) === 'token';\n};\n\nexport async function linkArgs<S extends StandardSchemaV1>(\n\t{ json }: Pick<LinksContext<string>, 'json'>,\n\tinvalidState: string,\n\tschema: S,\n\trequest: Request,\n\tpath: string,\n): Promise<InferOutput<S>> {\n\tsamesitePost(json, request);\n\tpathOf(path, 'linkArgs()');\n\tconst body = await json.readJson(request);\n\tconst result = await schema['~standard'].validate(body);\n\tif (!result.issues) return result.value as InferOutput<S>;\n\t// A token the API would refuse anyway (a link cut or padded on its way)\n\t// ends as an unknown one does; anything else is a bad request.\n\tif (result.issues.some(isTokenIssue)) {\n\t\tthrow stateRedirect(path, invalidState);\n\t}\n\tthrow data('Bad link request', { status: 400 });\n}\n\nexport async function spendToken<L extends string, T>(\n\toptions: LinksContext<L>,\n\trun: () => Promise<T>,\n\t{ request, language, path, states, fields: shown = [] }: SpendOptions<L>,\n): Promise<Spent<T>> {\n\tconst { errors } = options;\n\tsamesitePost(options.json, request);\n\tpathOf(path, 'spendToken()');\n\ttry {\n\t\treturn { ok: true, value: await run() };\n\t} catch (error) {\n\t\tconst refusal = errors.apiRefusal(error, language);\n\t\tif (!refusal) throw error;\n\t\tconst { message, status } = refusal;\n\t\tif (isApiErrorLike(error)) {\n\t\t\tconst state = error.code ? states[error.code] : undefined;\n\t\t\tif (state) return { ok: false, response: stateRedirect(path, state) };\n\t\t\tconst fields = errors.fieldErrors(error, language);\n\t\t\tconst named = Object.keys(fields);\n\t\t\t// A refusal under a field the page does not show would say nothing.\n\t\t\tif (named.length && named.every((field) => shown.includes(field))) {\n\t\t\t\treturn { ok: false, refusal: { fieldErrors: fields }, status };\n\t\t\t}\n\t\t}\n\t\treturn { ok: false, refusal: { formError: message }, status };\n\t}\n}\n\nexport const answerOf = (\n\tspent: Exclude<Spent<unknown>, { ok: true }>,\n): Response | LinkStayed =>\n\t'response' in spent\n\t\t? spent.response\n\t\t: data<FormErrors>(spent.refusal, { status: spent.status });\n",
|
|
9
|
+
"import type { JsonActions } from '../errors/json-actions';\nimport { refuseCrossSite } from '../errors/same-site';\n\n/**\n * What every step that signs in, out, spends or mails runs first: a native\n * form post (before hydration) goes back to its page, which then posts JSON\n * (`json.requireJsonPost`), and a request from another site is a 403 even\n * when it is JSON (the `Origin` and `Sec-Fetch-Site` rule of `readJson`). It\n * reads headers only, never the body.\n */\nexport function samesitePost(json: JsonActions, request: Request): void {\n\tjson.requireJsonPost(request);\n\trefuseCrossSite(json, request);\n}\n",
|
|
10
|
+
"import { data } from 'react-router';\nimport {\n\ttype ApiErrorLike,\n\tisApiErrorLike,\n\tretryAfterOf,\n} from '../errors/api-error';\nimport { MAIL_THROTTLED } from './codes';\nimport { samesitePost } from './guard';\nimport { afterReset, landByLink } from './link-land';\nimport {\n\tanswerOf,\n\tlinkArgs,\n\tspendToken,\n\tstatePath,\n\tstateRedirect,\n} from './link-token';\nimport type {\n\tAuthLinks,\n\tAuthLinksOptions,\n\tLinksContext,\n\tRequestLinkAnswer,\n} from './link-types';\nimport { sitePath } from './model/site-path';\n\nconst RESET_DONE = '/sign-in?password-reset=1';\n\n/**\n * Requests a mail's link with `run`. The API answers true whether or not\n * the e-mail holds an account, and so does the page: the same \"sent\" for\n * everyone. A wait or an unreachable API is the form's message; a malformed\n * address is the `email` field's.\n */\n/** The refusal's message: the app's own wording, else `errors`'. */\nconst worded = <L extends string>(\n\t{ wording, errors }: LinksContext<L>,\n\terror: ApiErrorLike,\n\tlanguage: L,\n): string =>\n\twording?.(error, language) ?? errors.apiErrorMessage(error, language);\n\nasync function requestLink<L extends string>(\n\toptions: LinksContext<L>,\n\trun: () => Promise<unknown>,\n\t{ request, language }: Parameters<AuthLinks<L>['requestLink']>[1],\n) {\n\tconst { errors } = options;\n\tsamesitePost(options.json, request);\n\ttry {\n\t\tawait run();\n\t\treturn data<RequestLinkAnswer>({ sent: true });\n\t} catch (error) {\n\t\tconst api = isApiErrorLike(error);\n\t\tif (\n\t\t\tapi &&\n\t\t\terror.code === (options.codes?.mailThrottled ?? MAIL_THROTTLED)\n\t\t) {\n\t\t\tconst retryAfter = retryAfterOf(error);\n\t\t\treturn data<RequestLinkAnswer>(\n\t\t\t\t{\n\t\t\t\t\tformError: worded(options, error, language),\n\t\t\t\t\tthrottled: true,\n\t\t\t\t\t...(retryAfter === undefined ? {} : { retryAfter }),\n\t\t\t\t},\n\t\t\t\t{ status: 429 },\n\t\t\t);\n\t\t}\n\t\tif (api) {\n\t\t\tconst email = errors.fieldErrors(error, language)['email'];\n\t\t\tif (email) {\n\t\t\t\treturn data<RequestLinkAnswer>(\n\t\t\t\t\t{ fieldErrors: { email } },\n\t\t\t\t\t{ status: error.status },\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tconst refusal = errors.apiRefusal(error, language);\n\t\tif (!refusal) throw error;\n\t\treturn data<RequestLinkAnswer>(\n\t\t\t{ formError: api ? worded(options, error, language) : refusal.message },\n\t\t\t{ status: refusal.status },\n\t\t);\n\t}\n}\n\n/**\n * The pages a mail's link opens (a magic link, a password reset, an\n * invitation): the token page's end states, the arguments it posts, the\n * token spent against the API, the sign-in it leads to (replacing the\n * token's history entry), and the request for a link. Built once, beside the\n * app's `signInFlow`.\n *\n * @example\n * export const links = authLinks({ signIn });\n * // loader: return data({ ... }, { headers: TOKEN_PAGE_HEADERS });\n */\nexport function authLinks<L extends string>(\n\tlinks: AuthLinksOptions<L>,\n): AuthLinks<L> {\n\tconst { signIn } = links;\n\tconst options: LinksContext<L> = {\n\t\t...links,\n\t\tcookies: signIn.cookies,\n\t\terrors: signIn.errors,\n\t\tjson: signIn.json,\n\t};\n\tconst resetDone = options.resetDone ?? RESET_DONE;\n\tsitePath(resetDone, { label: 'authLinks(): resetDone', query: true });\n\tconst invalid = options.invalidState ?? 'invalid';\n\treturn {\n\t\tstatePath,\n\t\tended: stateRedirect,\n\t\tlinkArgs: (schema, request, path) =>\n\t\t\tlinkArgs(options, invalid, schema, request, path),\n\t\tspendToken: (run, o) => spendToken(options, run, o),\n\t\tanswerOf,\n\t\tlandByLink: (answer, o) => landByLink(options, answer, o),\n\t\tafterReset: (request) => afterReset(options, resetDone, request),\n\t\trequestLink: (run, o) => requestLink(options, run, o),\n\t};\n}\n",
|
|
11
|
+
"import { data, replace } from 'react-router';\nimport type { FormErrors } from '../errors/form-errors';\nimport type { SignInAnswer } from './flow-types';\nimport { samesitePost } from './guard';\nimport { isLive } from './land';\nimport { stateRedirect, withTokenHeaders } from './link-token';\nimport type { AuthLinks, LinkStayed, LinksContext } from './link-types';\nimport { HOME, safeNext } from './model';\n\n/** Where a new session starts: the app's start page, else `/`; never an unsafe path. */\nasync function startOf<L extends string>(\n\t{ start }: LinksContext<L>,\n\trequest: Request,\n\ttoken: string,\n): Promise<string> {\n\tif (!start) return HOME;\n\ttry {\n\t\treturn safeNext(await start(token, request));\n\t} catch {\n\t\t// The token is spent: a failed lookup must not fail the sign-in.\n\t\treturn HOME;\n\t}\n}\n\n/**\n * Where a link's sign-in leads, replacing the token's history entry. Signed\n * in: the start page, with the new session's cookie; a second factor: its\n * page. Either way the session the browser held, whoever's, is revoked\n * first: one browser holds one session. Only a JSON post goes on, before\n * anything is revoked or landed.\n */\nexport async function landByLink<L extends string>(\n\toptions: LinksContext<L>,\n\tanswer: SignInAnswer,\n\t{ request, language, other }: Parameters<AuthLinks<L>['landByLink']>[1],\n): Promise<Response | LinkStayed> {\n\tsamesitePost(options.json, request);\n\tif (answer.status === 'other' && other) {\n\t\treturn stateRedirect(other.path, other.state);\n\t}\n\t// A dead session would be looked up with a token the API refuses.\n\tconst next = isLive(answer)\n\t\t? await startOf(options, request, answer.token)\n\t\t: HOME;\n\tconst landed = await options.signIn.landReplacing(answer, next, {\n\t\trequest,\n\t\trevokeOn: ['signedIn', 'secondFactor'],\n\t});\n\tif (!landed) {\n\t\treturn data<FormErrors>(\n\t\t\t{ formError: options.signIn.unavailable(language) },\n\t\t\t{ status: 503 },\n\t\t);\n\t}\n\tconst location = landed.headers.get('Location') ?? HOME;\n\treturn replace(location, { headers: withTokenHeaders(landed.headers) });\n}\n\n/**\n * After a password reset, which signs the account out everywhere: the\n * browser's session is revoked (it may be another account's) and its cookies\n * cleared (the session's, the challenge's, the flow's `clear()`), then the sign-in page says the password changed. Only a JSON post\n * goes on.\n */\nexport async function afterReset<L extends string>(\n\toptions: LinksContext<L>,\n\tresetDone: string,\n\trequest: Request,\n): Promise<Response> {\n\tsamesitePost(options.json, request);\n\tawait options.signIn.revokeSession(request);\n\tconst headers = withTokenHeaders();\n\theaders.append('Set-Cookie', options.cookies.destroy());\n\theaders.append('Set-Cookie', options.cookies.destroyChallenge());\n\tfor (const cookie of options.signIn.clear()) {\n\t\theaders.append('Set-Cookie', cookie);\n\t}\n\treturn replace(resetDone, { headers });\n}\n",
|
|
12
|
+
"import { redirect } from 'react-router';\nimport type { FlowContext, SignInAnswer, SignInFlow } from './flow-types';\nimport { samesitePost } from './guard';\nimport { safeNext, secondFactorPath } from './model';\nimport { millisOf } from './serialize';\n\n/**\n * Ends the request's session at the API, when it carries one; a failure is\n * ignored, the caller clears the cookie anyway. True when there was one.\n */\nexport async function revokeSession<L extends string>(\n\t{ options }: FlowContext<L>,\n\trequest: Request,\n): Promise<boolean> {\n\tsamesitePost(options.json, request);\n\tconst token = options.cookies.token(request);\n\tif (!token) return false;\n\ttry {\n\t\tawait options.revoke(token, request);\n\t} catch {\n\t\t// The old session ends when it expires.\n\t}\n\treturn true;\n}\n\n/** The condition a session opens on: a signed-in answer with a token and an end still to come. */\nexport const isLive = (\n\tanswer: SignInAnswer,\n): answer is Extract<SignInAnswer, { status: 'signedIn' }> =>\n\tanswer.status === 'signedIn' &&\n\tBoolean(answer.token) &&\n\tBoolean(answer.expiresAt) &&\n\tmillisOf(answer.expiresAt) > Date.now();\n\n/**\n * Where a sign-in's answer leads: signed in, to `next` with the session\n * cookie (and the challenge cleared); a second factor, to its page with the\n * challenge in its own cookie, never in the URL. Any other answer, or a\n * signed-in one with no token or an end that is missing, unreadable or past,\n * opens no session: `null`, the caller shows its own state.\n */\nexport function landing<L extends string>(\n\t{ options, paths }: FlowContext<L>,\n\tanswer: SignInAnswer,\n\tnext: string,\n): Response | null {\n\tconst { cookies } = options;\n\t// A session with no readable or past end would be set and dead at once.\n\tif (isLive(answer)) {\n\t\tconst headers = new Headers();\n\t\theaders.append(\n\t\t\t'Set-Cookie',\n\t\t\tcookies.commit(answer.token, answer.expiresAt),\n\t\t);\n\t\theaders.append('Set-Cookie', cookies.destroyChallenge());\n\t\tfor (const cookie of options.clear?.() ?? []) {\n\t\t\theaders.append('Set-Cookie', cookie);\n\t\t}\n\t\treturn redirect(safeNext(next), { headers });\n\t}\n\tif (answer.status === 'secondFactor' && answer.challenge) {\n\t\treturn redirect(secondFactorPath(next, paths.secondFactor), {\n\t\t\theaders: {\n\t\t\t\t'Set-Cookie': cookies.commitChallenge(\n\t\t\t\t\tanswer.challenge,\n\t\t\t\t\tanswer.expiresAt,\n\t\t\t\t),\n\t\t\t},\n\t\t});\n\t}\n\treturn null;\n}\n\n/**\n * `landing`, ending the session the browser held first: one browser holds\n * one session, so the one its cookie names, whoever's, is revoked when an\n * answer in `revokeOn` lands. A signed-in answer's own cookie replaces the\n * old one; any other answer clears it. A failed revoke is ignored.\n */\nexport async function landReplacing<L extends string>(\n\tcontext: FlowContext<L>,\n\tanswer: SignInAnswer,\n\tnext: string,\n\t{\n\t\trequest,\n\t\trevokeOn = ['signedIn'],\n\t}: Parameters<SignInFlow<L>['landReplacing']>[2],\n): Promise<Response | null> {\n\tsamesitePost(context.options.json, request);\n\tconst landed = landing(context, answer, next);\n\tif (!landed || answer.status === 'other') return landed;\n\tif (!revokeOn.includes(answer.status)) return landed;\n\tconst hadSession = await revokeSession(context, request);\n\tif (hadSession && answer.status !== 'signedIn') {\n\t\tlanded.headers.append('Set-Cookie', context.options.cookies.destroy());\n\t}\n\treturn landed;\n}\n",
|
|
13
|
+
"import { data } from 'react-router';\nimport type { FlowContext, SignInAnswer, SignInFlow } from './flow-types';\nimport { samesitePost } from './guard';\nimport type { SecondFactorKind } from './model';\n\nconst MAX_CODE = 40;\n\nfunction isMethod(value: unknown): value is SecondFactorKind {\n\treturn value === 'code' || value === 'recovery';\n}\n\n/**\n * The code a second-factor page posted, with how it is given\n * (`{ kind: 'code' | 'recovery', code }`, the code at most 40 characters). A\n * native form post goes back to its page, which then posts JSON; a\n * cross-site post is a 403; a body of another shape is a 400.\n */\nexport async function readCodeInput<L extends string>(\n\t{ options }: FlowContext<L>,\n\trequest: Request,\n): Promise<{ kind: SecondFactorKind; code: string }> {\n\tsamesitePost(options.json, request);\n\tconst body = (await options.json.readJson(request)) as {\n\t\tkind?: unknown;\n\t\tcode?: unknown;\n\t} | null;\n\tconst { kind, code } = body ?? {};\n\tif (!isMethod(kind) || typeof code !== 'string' || code.length > MAX_CODE) {\n\t\tthrow data('Bad code request', { status: 400 });\n\t}\n\treturn { kind, code };\n}\n\n/**\n * The code against the sign-in's challenge: an issued code (an app's, or\n * the one mailed) is confirmed, a recovery code is recovered. Either\n * answers like a sign-in.\n */\nexport function codeStep(\n\tinput: { readonly kind: SecondFactorKind; readonly code: string },\n\tchallenge: string,\n\tsteps: Parameters<SignInFlow<string>['codeStep']>[2],\n): Promise<SignInAnswer> {\n\treturn input.kind === 'code'\n\t\t? steps.confirm(challenge, input.code)\n\t\t: steps.recover(challenge, input.code);\n}\n",
|
|
14
|
+
"import { data } from 'react-router';\nimport {\n\ttype ApiErrorLike,\n\tattemptsLeftOf,\n\tisApiErrorLike,\n} from '../errors/api-error';\nimport type {\n\tCodeWording,\n\tFlowContext,\n\tSignInAnswer,\n\tSignInFlow,\n\tSignInFlowOptions,\n\tSignInFormError,\n\tSignInStayed,\n} from './flow-types';\nimport { samesitePost } from './guard';\nimport { landReplacing } from './land';\n\n/**\n * A refused code's message, with the tries left when the API counts them;\n * `undefined` when the challenge is over (gone, or no try left and no\n * `whenNone` to say instead) and the user must sign in again.\n */\nexport function codeRefusal<L extends string>(\n\t{ options, invalidCode, challengeOver }: CodeWording<L>,\n\terror: ApiErrorLike,\n\tlanguage: L,\n\twhenNone?: string,\n): string | undefined {\n\tif (error.code !== invalidCode) {\n\t\treturn error.code && challengeOver.includes(error.code)\n\t\t\t? undefined\n\t\t\t: options.errors.apiErrorMessage(error, language);\n\t}\n\tconst message = options.errors.apiErrorMessage(error, language);\n\tconst count = attemptsLeftOf(error);\n\tif (count === undefined) return message;\n\tif (count === 0) {\n\t\treturn whenNone === undefined ? undefined : `${message} ${whenNone}`;\n\t}\n\treturn `${message} ${options.attemptsLeft(count, language)}`;\n}\n\n/** What a sign-in says when the API cannot be reached. */\nexport const unavailableMessage = <L extends string>(\n\t{ unavailable, errors }: Pick<SignInFlowOptions<L>, 'unavailable' | 'errors'>,\n\tlanguage: L,\n): string => unavailable?.(language) ?? errors.unavailableMessage(language);\n\n/**\n * Runs a sign-in step and redirects where its answer leads, a new session\n * replacing the browser's (`landReplacing`). A refusal is the form's\n * message (a wrong password, a lock with its delay); an unreachable API\n * says `unavailable`, with a 503.\n */\nexport async function signInStep<L extends string, T extends object>(\n\tcontext: FlowContext<L>,\n\trun: () => Promise<SignInAnswer>,\n\t{\n\t\trequest,\n\t\tlanguage,\n\t\tnext,\n\t\textra,\n\t\trefused,\n\t}: Parameters<SignInFlow<L>['signInStep']>[1] & { extra: T },\n): Promise<Response | SignInStayed<T>> {\n\tconst { errors, json } = context.options;\n\t// A cross-site post must never sign in or revoke (headers only).\n\tsamesitePost(json, request);\n\tconst stay = (formError: string, status: number) =>\n\t\tdata<SignInFormError<T>>({ ...extra, formError }, { status });\n\tconst unreachable = () =>\n\t\tstay(unavailableMessage(context.options, language), 503);\n\tlet answer: SignInAnswer;\n\ttry {\n\t\tanswer = await run();\n\t} catch (error) {\n\t\tconst refusal = errors.apiRefusal(error, language);\n\t\tif (!refusal) throw error;\n\t\t// An API it cannot reach: sign-in's own words.\n\t\tif (!isApiErrorLike(error)) return unreachable();\n\t\tconst own = refused?.(error);\n\t\tif (own instanceof Response) return own;\n\t\treturn stay(own ?? refusal.message, refusal.status);\n\t}\n\treturn (\n\t\t(await landReplacing(context, answer, next, { request })) ?? unreachable()\n\t);\n}\n",
|
|
15
|
+
"import { redirect } from 'react-router';\nimport { isApiErrorLike } from '../errors/api-error';\nimport { nextOf } from '../errors/page-path';\nimport { routeError } from '../errors/route-error';\nimport type { FlowContext } from './flow-types';\nimport { revokeSession } from './land';\nimport { safeNext, signInPath } from './model';\n\n/** The `next` a sign-in page was opened with, safe to redirect to. */\nexport const nextParam = (request: Request): string =>\n\tsafeNext(new URL(request.url).searchParams.get('next'));\n\n/**\n * The sign-in page, coming back to this request's page afterwards, with the\n * session cookie cleared: what a page answers without a (live) session. A\n * request that carried a session cookie says it ended when `endedParam` is\n * set.\n */\nexport function signInRedirect<L extends string>(\n\t{ options, paths }: FlowContext<L>,\n\trequest: Request,\n): Response {\n\tconst { endedParam, cookies } = options;\n\tconst query =\n\t\tendedParam && cookies.read(request) ? { [endedParam]: '1' } : undefined;\n\treturn redirect(\n\t\tsignInPath(nextOf(request.url), {\n\t\t\tpath: paths.signIn,\n\t\t\t...(query && { query }),\n\t\t}),\n\t\t{ headers: { 'Set-Cookie': cookies.destroy() } },\n\t);\n}\n\n/**\n * For the sign-in pages: the viewer when the cookie holds a live session,\n * `null` when the API refuses it (a 401), `undefined` without a cookie or\n * when the API cannot tell.\n */\nexport async function viewer<L extends string, V>(\n\t{ options }: FlowContext<L>,\n\trequest: Request,\n\tread: (token: string) => Promise<V | null>,\n\ttoken: string | null | undefined = options.cookies.token(request),\n): Promise<V | null | undefined> {\n\tif (!token) return undefined;\n\ttry {\n\t\treturn await read(token);\n\t} catch (error) {\n\t\treturn isApiErrorLike(error) && error.status === 401 ? null : undefined;\n\t}\n}\n\n/**\n * The signed-in viewer, else the sign-in redirect. Its `headers` move the\n * cookie's expiry to the session's when the API moved it.\n */\nexport async function requireViewer<L extends string, V>(\n\tcontext: FlowContext<L>,\n\trequest: Request,\n\tread: (token: string) => Promise<V | null>,\n\texpiresAt: (viewer: V) => string | null,\n): Promise<{ viewer: V; headers: HeadersInit }> {\n\tconst session = context.options.cookies.read(request);\n\tif (!session) throw signInRedirect(context, request);\n\tlet found: V | null;\n\ttry {\n\t\tfound = await read(session.token);\n\t} catch (error) {\n\t\tif (isApiErrorLike(error) && error.status === 401) {\n\t\t\tthrow signInRedirect(context, request);\n\t\t}\n\t\tthrow routeError(error);\n\t}\n\tif (!found) throw signInRedirect(context, request);\n\treturn {\n\t\tviewer: found,\n\t\theaders: context.options.cookies.refreshed(session, expiresAt(found)),\n\t};\n}\n\n/**\n * Ends the session and goes to the sign-in page (`?signed-out=1`), or to\n * `to`. The body is read as JSON first: JSON and same-origin only, so\n * another site cannot sign the user out.\n */\nexport async function signOut<L extends string>(\n\tcontext: FlowContext<L>,\n\trequest: Request,\n\t{ to }: { readonly to?: string | undefined } = {},\n): Promise<Response> {\n\tconst { options, paths } = context;\n\tawait options.json.readJson(request);\n\tawait revokeSession(context, request);\n\tconst headers = new Headers();\n\theaders.append('Set-Cookie', options.cookies.destroy());\n\theaders.append('Set-Cookie', options.cookies.destroyChallenge());\n\tfor (const cookie of options.clear?.() ?? []) {\n\t\theaders.append('Set-Cookie', cookie);\n\t}\n\treturn redirect(\n\t\tto === undefined ? `${paths.signIn}?signed-out=1` : safeNext(to),\n\t\t{ headers },\n\t);\n}\n",
|
|
16
|
+
"import { hasSiteCheck } from '../errors/same-site';\nimport { CHALLENGE_OVER, INVALID_CODE } from './codes';\nimport type { FlowContext, SignInFlow, SignInFlowOptions } from './flow-types';\nimport { samesitePost } from './guard';\nimport { landing, landReplacing, revokeSession } from './land';\nimport { SIGN_IN_PATHS, type SignInPaths } from './model';\nimport { sitePath } from './model/site-path';\nimport { codeStep, readCodeInput } from './second-factor';\nimport { codeRefusal, signInStep, unavailableMessage } from './step';\nimport {\n\tnextParam,\n\trequireViewer,\n\tsignInRedirect,\n\tsignOut,\n\tviewer,\n} from './viewer';\n\nfunction resolve<L extends string>(\n\toptions: SignInFlowOptions<L>,\n): FlowContext<L> {\n\tconst paths: SignInPaths = { ...SIGN_IN_PATHS, ...options.paths };\n\tfor (const key of ['signIn', 'secondFactor'] as const) {\n\t\tsitePath(paths[key], { label: `signInFlow(): paths.${key}` });\n\t}\n\tif (!hasSiteCheck(options.json)) {\n\t\tthrow new TypeError(\n\t\t\t\"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.\",\n\t\t);\n\t}\n\tconst invalidCode = options.codes?.invalidCode ?? INVALID_CODE;\n\tconst challengeOver = options.codes?.challengeOver ?? CHALLENGE_OVER;\n\tif (challengeOver.includes(invalidCode)) {\n\t\tthrow new TypeError(\n\t\t\t`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.`,\n\t\t);\n\t}\n\treturn { options, paths, invalidCode, challengeOver };\n}\n\n/**\n * The server side of a BFF's sign-in, over the cookies, the app's\n * `createApiErrors` and its `jsonActions`: where an answer lands, the form\n * steps with their refusals, the second factor, the viewer and sign-out.\n * Built once, beside the app's cookies; it speaks `@nxgt/janus`'s words\n * (`signedIn`, `secondFactor`, `CODE_INVALID`) and the app maps its API's.\n *\n * @example\n * export const signIn = signInFlow({\n * \tcookies, errors, json,\n * \trevoke: (token, request) => execute(SignOutMutation, {}, { token }),\n * \tattemptsLeft: (count, language) => t('auth.attempts-left', { count }, language),\n * });\n */\nexport function signInFlow<L extends string>(\n\toptions: SignInFlowOptions<L>,\n): SignInFlow<L> {\n\tconst context = resolve(options);\n\tconst { options: o } = context;\n\treturn {\n\t\tcookies: o.cookies,\n\t\terrors: o.errors,\n\t\tjson: o.json,\n\t\tunavailable: (language) => unavailableMessage(o, language),\n\t\tclear: () => o.clear?.() ?? [],\n\t\tattemptsLeft: o.attemptsLeft,\n\t\tguard: (request) => samesitePost(o.json, request),\n\t\tnextParam,\n\t\tsignInRedirect: (request) => signInRedirect(context, request),\n\t\tlanding: (answer, next) => landing(context, answer, next),\n\t\tlandReplacing: (answer, next, o) => landReplacing(context, answer, next, o),\n\t\tsignInStep: (run, o) => signInStep(context, run, o),\n\t\tcodeRefusal: (error, language, whenNone) =>\n\t\t\tcodeRefusal(context, error, language, whenNone),\n\t\treadCodeInput: (request) => readCodeInput(context, request),\n\t\tcodeStep,\n\t\trevokeSession: (request) => revokeSession(context, request),\n\t\tsignOut: (request, o) => signOut(context, request, o),\n\t\tviewer: (request, read, token) => viewer(context, request, read, token),\n\t\trequireViewer: (request, read, expiresAt) =>\n\t\t\trequireViewer(context, request, read, expiresAt),\n\t};\n}\n",
|
|
17
|
+
"import { data } from 'react-router';\nimport {\n\ttype ApiErrorLike,\n\tattemptsLeftOf,\n\tisApiErrorLike,\n\tretryAfterOf,\n} from '../errors/api-error';\nimport { INVALID_CODE, MAIL_THROTTLED } from './codes';\nimport type { CodeWording } from './flow-types';\nimport { samesitePost } from './guard';\nimport type {\n\tStepUpConfirmAnswer,\n\tStepUpError,\n\tStepUpRequestAnswer,\n\tStepUpVia,\n} from './model';\nimport { codeRefusal } from './step';\nimport { STEP_UP_OVER, STEP_UP_REQUIRED } from './step-up-codes';\nimport type {\n\tStepUpStayed,\n\tStepUpSteps,\n\tStepUpStepsOptions,\n} from './step-up-types';\n\ntype Refused = Extract<StepUpConfirmAnswer, { ok: false }>;\n\ninterface StepUpContext<L extends string> extends CodeWording<L> {\n\treadonly steps: StepUpStepsOptions<L>;\n\treadonly stepUpRequired: string;\n\treadonly mailThrottled: string;\n}\n\nfunction resolve<L extends string>(\n\tsteps: StepUpStepsOptions<L>,\n): StepUpContext<L> {\n\tconst invalidCode = steps.codes?.invalidCode ?? INVALID_CODE;\n\tconst challengeOver = steps.codes?.challengeOver ?? STEP_UP_OVER;\n\tif (challengeOver.includes(invalidCode)) {\n\t\tthrow new TypeError(\n\t\t\t`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.`,\n\t\t);\n\t}\n\treturn {\n\t\toptions: steps.signIn,\n\t\tsteps,\n\t\tinvalidCode,\n\t\tchallengeOver,\n\t\tstepUpRequired: steps.codes?.stepUpRequired ?? STEP_UP_REQUIRED,\n\t\tmailThrottled: steps.codes?.mailThrottled ?? MAIL_THROTTLED,\n\t};\n}\n\n/** The refusal's message: the app's own wording, else `errors`'. */\nfunction worded<L extends string>(\n\t{ steps }: StepUpContext<L>,\n\terror: ApiErrorLike,\n\tlanguage: L,\n): string {\n\treturn (\n\t\tsteps.wording?.(error, language) ??\n\t\tsteps.signIn.errors.apiErrorMessage(error, language)\n\t);\n}\n\n/** A code the API refused. With none left, a mailed one is over and an app's is locked. */\nfunction refusedCode<L extends string>(\n\tcontext: StepUpContext<L>,\n\terror: ApiErrorLike,\n\tvia: StepUpVia | undefined,\n\tlanguage: L,\n): Omit<Refused, 'ok' | 'intent'> {\n\tconst { steps } = context;\n\tconst attemptsLeft = attemptsLeftOf(error);\n\tif (attemptsLeft === 0 && via !== 'secondFactor') {\n\t\treturn {\n\t\t\terror: 'expired',\n\t\t\tattemptsLeft,\n\t\t\tlocked: false,\n\t\t\tmessage: steps.spent(language),\n\t\t};\n\t}\n\tconst locked = attemptsLeft === 0;\n\tconst message =\n\t\tcodeRefusal(\n\t\t\tcontext,\n\t\t\terror,\n\t\t\tlanguage,\n\t\t\tlocked ? steps.locked?.(language) : undefined,\n\t\t) ?? steps.signIn.errors.apiErrorMessage(error, language);\n\treturn {\n\t\terror: 'invalid',\n\t\t...(attemptsLeft === undefined ? {} : { attemptsLeft }),\n\t\tlocked,\n\t\tmessage,\n\t};\n}\n\nasync function requestStep<L extends string>(\n\tcontext: StepUpContext<L>,\n\trun: Parameters<StepUpSteps<L>['request']>[0],\n\t{ request, language }: Parameters<StepUpSteps<L>['request']>[1],\n): Promise<StepUpRequestAnswer | StepUpStayed<StepUpRequestAnswer>> {\n\tsamesitePost(context.steps.signIn.json, request);\n\tconst intent = 'request-step-up';\n\ttry {\n\t\tconst { challenge, via } = await run();\n\t\treturn { ok: true, intent, challenge, via };\n\t} catch (error) {\n\t\tconst refusal = context.steps.signIn.errors.apiRefusal(error, language);\n\t\tif (!refusal) throw error;\n\t\tconst api = isApiErrorLike(error);\n\t\tconst message = api ? worded(context, error, language) : refusal.message;\n\t\tconst throttled = api && error.code === context.mailThrottled;\n\t\tconst retryAfter = throttled ? retryAfterOf(error) : undefined;\n\t\treturn data<StepUpRequestAnswer>(\n\t\t\t{\n\t\t\t\tok: false,\n\t\t\t\tintent,\n\t\t\t\terror: throttled ? 'throttled' : 'failed',\n\t\t\t\tmessage,\n\t\t\t\t...(retryAfter === undefined ? {} : { retryAfter }),\n\t\t\t},\n\t\t\t{ status: refusal.status },\n\t\t);\n\t}\n}\n\nasync function confirmStep<L extends string>(\n\tcontext: StepUpContext<L>,\n\trun: () => Promise<unknown>,\n\t{ request, language, via }: Parameters<StepUpSteps<L>['confirm']>[1],\n): Promise<StepUpConfirmAnswer | StepUpStayed<StepUpConfirmAnswer>> {\n\tsamesitePost(context.steps.signIn.json, request);\n\tconst intent = 'confirm-step-up';\n\ttry {\n\t\tawait run();\n\t\treturn { ok: true, intent };\n\t} catch (error) {\n\t\tconst refusal = context.steps.signIn.errors.apiRefusal(error, language);\n\t\tif (!refusal) throw error;\n\t\tconst stay = (\n\t\t\tfailure: Omit<Refused, 'ok' | 'intent'>,\n\t\t): StepUpStayed<StepUpConfirmAnswer> =>\n\t\t\tdata<StepUpConfirmAnswer>(\n\t\t\t\t{ ok: false, intent, ...failure },\n\t\t\t\t{ status: refusal.status },\n\t\t\t);\n\t\tconst failed = (message: string) =>\n\t\t\tstay({ error: 'failed' satisfies StepUpError, locked: false, message });\n\t\tif (!isApiErrorLike(error)) return failed(refusal.message);\n\t\tif (error.code === context.invalidCode) {\n\t\t\treturn stay(refusedCode(context, error, via, language));\n\t\t}\n\t\tif (error.code && context.challengeOver.includes(error.code)) {\n\t\t\treturn stay({\n\t\t\t\terror: 'expired',\n\t\t\t\tlocked: false,\n\t\t\t\tmessage: context.steps.spent(language),\n\t\t\t});\n\t\t}\n\t\treturn failed(worded(context, error, language));\n\t}\n}\n\n/**\n * The server side of a BFF's step-up: `request` and `confirm` answer the\n * page's fetcher with `StepUpRequestAnswer` and `StepUpConfirmAnswer`, which\n * `stepUpReducer` takes as they are. It speaks `@nxgt/janus`'s words\n * (`CODE_INVALID`, `TOKEN_*`, `STEP_UP_REQUIRED`); the app maps the rest.\n *\n * @example\n * const stepUp = stepUpSteps({\n * \terrors,\n * \tattemptsLeft: (count, language) => t('code.attempts-left', { count }, language),\n * \tspent: (language) => t('step-up.spent', {}, language),\n * \tlocked: (language) => t('step-up.locked', {}, language),\n * });\n */\nexport function stepUpSteps<L extends string>(\n\toptions: StepUpStepsOptions<L>,\n): StepUpSteps<L> {\n\tconst context = resolve(options);\n\treturn {\n\t\trequest: (run, o) => requestStep(context, run, o),\n\t\tconfirm: (run, o) => confirmStep(context, run, o),\n\t\tisStepUpRequired: (error) =>\n\t\t\tisApiErrorLike(error) && error.code === context.stepUpRequired,\n\t};\n}\n",
|
|
18
|
+
"import { CHALLENGE_OVER } from './codes';\n\n/** `@nxgt/janus`'s code for a mutation that needs a recent step-up. */\nexport const STEP_UP_REQUIRED = 'STEP_UP_REQUIRED';\n\n/**\n * The codes that end a step-up's challenge: unknown, spent, expired or stale,\n * and a second factor that changed since the request (`SECOND_FACTOR_ACTIVE`,\n * `SECOND_FACTOR_NOT_ENROLLED`). The user asks again.\n */\nexport const STEP_UP_OVER = [\n\t...CHALLENGE_OVER,\n\t'TOKEN_STALE',\n\t'SECOND_FACTOR_ACTIVE',\n\t'SECOND_FACTOR_NOT_ENROLLED',\n] as const;\n"
|
|
19
|
+
],
|
|
20
|
+
"mappings": "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",
|
|
21
|
+
"debugId": "AAF047E47EB9CAF964756E2164756E21",
|
|
22
|
+
"names": []
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FlowContext, SignInAnswer, SignInFlow } from './flow-types';
|
|
2
|
+
/**
|
|
3
|
+
* Ends the request's session at the API, when it carries one; a failure is
|
|
4
|
+
* ignored, the caller clears the cookie anyway. True when there was one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function revokeSession<L extends string>({ options }: FlowContext<L>, request: Request): Promise<boolean>;
|
|
7
|
+
/** The condition a session opens on: a signed-in answer with a token and an end still to come. */
|
|
8
|
+
export declare const isLive: (answer: SignInAnswer) => answer is Extract<SignInAnswer, {
|
|
9
|
+
status: "signedIn";
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Where a sign-in's answer leads: signed in, to `next` with the session
|
|
13
|
+
* cookie (and the challenge cleared); a second factor, to its page with the
|
|
14
|
+
* challenge in its own cookie, never in the URL. Any other answer, or a
|
|
15
|
+
* signed-in one with no token or an end that is missing, unreadable or past,
|
|
16
|
+
* opens no session: `null`, the caller shows its own state.
|
|
17
|
+
*/
|
|
18
|
+
export declare function landing<L extends string>({ options, paths }: FlowContext<L>, answer: SignInAnswer, next: string): Response | null;
|
|
19
|
+
/**
|
|
20
|
+
* `landing`, ending the session the browser held first: one browser holds
|
|
21
|
+
* one session, so the one its cookie names, whoever's, is revoked when an
|
|
22
|
+
* answer in `revokeOn` lands. A signed-in answer's own cookie replaces the
|
|
23
|
+
* old one; any other answer clears it. A failed revoke is ignored.
|
|
24
|
+
*/
|
|
25
|
+
export declare function landReplacing<L extends string>(context: FlowContext<L>, answer: SignInAnswer, next: string, { request, revokeOn, }: Parameters<SignInFlow<L>['landReplacing']>[2]): Promise<Response | null>;
|
|
26
|
+
//# sourceMappingURL=land.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"land.d.ts","sourceRoot":"","sources":["../../src/session/land.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK1E;;;GAGG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS,MAAM,EACnD,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,kGAAkG;AAClG,eAAO,MAAM,MAAM,GAClB,QAAQ,YAAY,KAClB,MAAM,IAAI,OAAO,CAAC,YAAY,EAAE;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAIjB,CAAC;AAEzC;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EACvC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAClC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,GACV,QAAQ,GAAG,IAAI,CA0BjB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS,MAAM,EACnD,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EACvB,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,EACC,OAAO,EACP,QAAuB,GACvB,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAU1B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SignInAnswer } from './flow-types';
|
|
2
|
+
import type { AuthLinks, LinkStayed, LinksContext } from './link-types';
|
|
3
|
+
/**
|
|
4
|
+
* Where a link's sign-in leads, replacing the token's history entry. Signed
|
|
5
|
+
* in: the start page, with the new session's cookie; a second factor: its
|
|
6
|
+
* page. Either way the session the browser held, whoever's, is revoked
|
|
7
|
+
* first: one browser holds one session. Only a JSON post goes on, before
|
|
8
|
+
* anything is revoked or landed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function landByLink<L extends string>(options: LinksContext<L>, answer: SignInAnswer, { request, language, other }: Parameters<AuthLinks<L>['landByLink']>[1]): Promise<Response | LinkStayed>;
|
|
11
|
+
/**
|
|
12
|
+
* After a password reset, which signs the account out everywhere: the
|
|
13
|
+
* browser's session is revoked (it may be another account's) and its cookies
|
|
14
|
+
* cleared (the session's, the challenge's, the flow's `clear()`), then the sign-in page says the password changed. Only a JSON post
|
|
15
|
+
* goes on.
|
|
16
|
+
*/
|
|
17
|
+
export declare function afterReset<L extends string>(options: LinksContext<L>, resetDone: string, request: Request): Promise<Response>;
|
|
18
|
+
//# sourceMappingURL=link-land.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-land.d.ts","sourceRoot":"","sources":["../../src/session/link-land.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkBxE;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,CAAC,SAAS,MAAM,EAChD,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EACxB,MAAM,EAAE,YAAY,EACpB,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GACrE,OAAO,CAAC,QAAQ,GAAG,UAAU,CAAC,CAqBhC;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,CAAC,SAAS,MAAM,EAChD,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EACxB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InferOutput, StandardSchemaV1 } from '../errors/standard-schema';
|
|
2
|
+
import type { LinkStayed, LinksContext, SpendOptions, Spent } from './link-types';
|
|
3
|
+
/**
|
|
4
|
+
* The response headers of a page a mail's link opens, whose URL carries a
|
|
5
|
+
* token: no `Referer` sends it to another site (its fonts included), and
|
|
6
|
+
* no search engine keeps it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const TOKEN_PAGE_HEADERS: {
|
|
9
|
+
readonly 'Referrer-Policy': "no-referrer";
|
|
10
|
+
readonly 'X-Robots-Tag': "noindex, nofollow";
|
|
11
|
+
};
|
|
12
|
+
export declare const statePath: (path: string, state: string) => string;
|
|
13
|
+
/** `headers`, with the token page's own on top. */
|
|
14
|
+
export declare function withTokenHeaders(headers?: HeadersInit): Headers;
|
|
15
|
+
/** A redirect replacing the current history entry: the token's, so Back never opens it again. */
|
|
16
|
+
export declare const stateRedirect: (path: string, state: string, headers?: HeadersInit) => Response;
|
|
17
|
+
export declare function linkArgs<S extends StandardSchemaV1>({ json }: Pick<LinksContext<string>, 'json'>, invalidState: string, schema: S, request: Request, path: string): Promise<InferOutput<S>>;
|
|
18
|
+
export declare function spendToken<L extends string, T>(options: LinksContext<L>, run: () => Promise<T>, { request, language, path, states, fields: shown }: SpendOptions<L>): Promise<Spent<T>>;
|
|
19
|
+
export declare const answerOf: (spent: Exclude<Spent<unknown>, {
|
|
20
|
+
ok: true;
|
|
21
|
+
}>) => Response | LinkStayed;
|
|
22
|
+
//# sourceMappingURL=link-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-token.d.ts","sourceRoot":"","sources":["../../src/session/link-token.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE/E,OAAO,KAAK,EACX,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,KAAK,EACL,MAAM,cAAc,CAAC;AAGtB;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;CAGrB,CAAC;AAMX,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,MACW,CAAC;AAEpE,mDAAmD;AACnD,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAM/D;AAED,iGAAiG;AACjG,eAAO,MAAM,aAAa,GACzB,MAAM,MAAM,EACZ,OAAO,MAAM,EACb,UAAU,WAAW,KACnB,QACqE,CAAC;AAWzE,wBAAsB,QAAQ,CAAC,CAAC,SAAS,gBAAgB,EACxD,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAC5C,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,CAAC,EACT,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAYzB;AAED,wBAAsB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACnD,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EACxB,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACrB,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAU,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,GACtE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAsBnB;AAED,eAAO,MAAM,QAAQ,GACpB,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,KAC1C,QAAQ,GAAG,UAG+C,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { data } from 'react-router';
|
|
2
|
+
import type { ApiErrorLike } from '../errors/api-error';
|
|
3
|
+
import type { FormErrors } from '../errors/form-errors';
|
|
4
|
+
import type { InferOutput, StandardSchemaV1 } from '../errors/standard-schema';
|
|
5
|
+
import type { SignInAnswer, SignInFlow } from './flow-types';
|
|
6
|
+
/** The codes the links tell apart; `@nxgt/janus`'s by default. */
|
|
7
|
+
export interface AuthLinksCodes {
|
|
8
|
+
/** The API's wait on mailed links; `MAIL_THROTTLED` by default. */
|
|
9
|
+
readonly mailThrottled?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface AuthLinksOptions<L extends string> {
|
|
12
|
+
/**
|
|
13
|
+
* The app's sign-in flow: it lands the sessions a link opens, and its
|
|
14
|
+
* `cookies`, `errors` and `json` are the ones the links use.
|
|
15
|
+
*/
|
|
16
|
+
readonly signIn: SignInFlow<L>;
|
|
17
|
+
/** The codes the links tell apart; `@nxgt/janus`'s by default. */
|
|
18
|
+
readonly codes?: AuthLinksCodes | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The app's own words for a refusal of `requestLink` that is not a field's
|
|
21
|
+
* (the wait of a throttled mail, say), before `errors`' default;
|
|
22
|
+
* `undefined` keeps the default.
|
|
23
|
+
*/
|
|
24
|
+
readonly wording?: ((error: ApiErrorLike, language: L) => string | undefined) | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Where a new session starts, from its token (the viewer's own page): read
|
|
27
|
+
* after the token is spent, so the sign-in lands there at once instead of
|
|
28
|
+
* on `/`, whose redirect would push a history entry after the token's.
|
|
29
|
+
* `/` when it is missing, throws or answers an unsafe path.
|
|
30
|
+
*/
|
|
31
|
+
readonly start?: ((token: string, request: Request) => Promise<string>) | undefined;
|
|
32
|
+
/** Where a password reset leads; `/sign-in?password-reset=1` by default. */
|
|
33
|
+
readonly resetDone?: string | undefined;
|
|
34
|
+
/** The state a token the API refuses ends on; `'invalid'` by default. */
|
|
35
|
+
readonly invalidState?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** The options with what they read of the flow: the functions take this. */
|
|
38
|
+
export type LinksContext<L extends string> = AuthLinksOptions<L> & Pick<SignInFlow<L>, 'cookies' | 'errors' | 'json'>;
|
|
39
|
+
/** What a token page answers when it stays on the page. */
|
|
40
|
+
export type LinkStayed = ReturnType<typeof data<FormErrors>>;
|
|
41
|
+
/** A link request's answer: always "sent", whoever the e-mail is. */
|
|
42
|
+
export type RequestLinkAnswer = {
|
|
43
|
+
sent?: true;
|
|
44
|
+
formError?: string;
|
|
45
|
+
/** The mail was held back for asking too often: `formError` says so, `retryAfter` how long. */
|
|
46
|
+
throttled?: true;
|
|
47
|
+
/** A throttled request: the seconds to wait (a whole number from 0), when the API says. */
|
|
48
|
+
retryAfter?: number;
|
|
49
|
+
/** The address refused as malformed: under the field. */
|
|
50
|
+
fieldErrors?: {
|
|
51
|
+
email?: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export type Spent<T> = {
|
|
55
|
+
readonly ok: true;
|
|
56
|
+
readonly value: T;
|
|
57
|
+
}
|
|
58
|
+
/** An end state of the page: the token no longer works. */
|
|
59
|
+
| {
|
|
60
|
+
readonly ok: false;
|
|
61
|
+
readonly response: Response;
|
|
62
|
+
}
|
|
63
|
+
/** The page stays, with the form's refusal. */
|
|
64
|
+
| {
|
|
65
|
+
readonly ok: false;
|
|
66
|
+
readonly refusal: FormErrors;
|
|
67
|
+
readonly status: number;
|
|
68
|
+
};
|
|
69
|
+
export interface SpendOptions<L extends string> {
|
|
70
|
+
/** The post: a same-site JSON one goes on, as for every step that spends. */
|
|
71
|
+
readonly request: Request;
|
|
72
|
+
readonly language: L;
|
|
73
|
+
/** The page's path, without its token. */
|
|
74
|
+
readonly path: string;
|
|
75
|
+
/** The API codes that end the page, and the state each one shows. */
|
|
76
|
+
readonly states: Readonly<Partial<Record<string, string>>>;
|
|
77
|
+
/** The fields the page shows a refusal under; any other is the form's message. */
|
|
78
|
+
readonly fields?: readonly string[] | undefined;
|
|
79
|
+
}
|
|
80
|
+
export interface AuthLinks<L extends string> {
|
|
81
|
+
/** The token page in an end state, without its token: `path?state=…`. */
|
|
82
|
+
statePath(path: string, state: string): string;
|
|
83
|
+
/** A redirect to a token page's end state, replacing the token's history entry. */
|
|
84
|
+
ended(path: string, state: string, headers?: HeadersInit): Response;
|
|
85
|
+
/**
|
|
86
|
+
* The arguments a token page posted, read as JSON: a native form post is
|
|
87
|
+
* redirected back to its page and a cross-site post is a 403 (before the
|
|
88
|
+
* body is read, so before anything is spent), then checked against
|
|
89
|
+
* a Standard Schema. A token the schema refuses ends on the invalid state
|
|
90
|
+
* (thrown); any other refusal is a 400.
|
|
91
|
+
*/
|
|
92
|
+
linkArgs<S extends StandardSchemaV1>(schema: S, request: Request, path: string): Promise<InferOutput<S>>;
|
|
93
|
+
/** Posts a link's token with `run`: its value, else what the page answers. A post that is not a same-site JSON one is refused first. */
|
|
94
|
+
spendToken<T>(run: () => Promise<T>, options: SpendOptions<L>): Promise<Spent<T>>;
|
|
95
|
+
/** What an action answers for a refused `Spent`. */
|
|
96
|
+
answerOf(spent: Exclude<Spent<unknown>, {
|
|
97
|
+
ok: true;
|
|
98
|
+
}>): Response | LinkStayed;
|
|
99
|
+
/**
|
|
100
|
+
* Where a link's sign-in leads, replacing the token's history entry. The
|
|
101
|
+
* session the browser held is revoked first; an answer that leads nowhere
|
|
102
|
+
* is the form's "unavailable"; `other` is too, unless `other` names the
|
|
103
|
+
* state to end on (an app's own `LINKED`: no session opened, the
|
|
104
|
+
* browser's kept). A native form post is redirected back to its page and a
|
|
105
|
+
* cross-site post is a 403, before anything is revoked or landed.
|
|
106
|
+
*/
|
|
107
|
+
landByLink(answer: SignInAnswer, options: {
|
|
108
|
+
readonly request: Request;
|
|
109
|
+
readonly language: L;
|
|
110
|
+
/** Where an `other` answer ends instead of failing: a token page and its state. */
|
|
111
|
+
readonly other?: {
|
|
112
|
+
readonly path: string;
|
|
113
|
+
readonly state: string;
|
|
114
|
+
} | undefined;
|
|
115
|
+
}): Promise<Response | LinkStayed>;
|
|
116
|
+
/** After a password reset: the browser's session revoked and cleared, then the sign-in page. */
|
|
117
|
+
afterReset(request: Request): Promise<Response>;
|
|
118
|
+
/** Requests a mail's link with `run`; only a same-site JSON post goes on. */
|
|
119
|
+
requestLink(run: () => Promise<unknown>, options: {
|
|
120
|
+
readonly request: Request;
|
|
121
|
+
readonly language: L;
|
|
122
|
+
}): Promise<ReturnType<typeof data<RequestLinkAnswer>>>;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=link-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-types.d.ts","sourceRoot":"","sources":["../../src/session/link-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthLinks, AuthLinksOptions } from './link-types';
|
|
2
|
+
/**
|
|
3
|
+
* The pages a mail's link opens (a magic link, a password reset, an
|
|
4
|
+
* invitation): the token page's end states, the arguments it posts, the
|
|
5
|
+
* token spent against the API, the sign-in it leads to (replacing the
|
|
6
|
+
* token's history entry), and the request for a link. Built once, beside the
|
|
7
|
+
* app's `signInFlow`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* export const links = authLinks({ signIn });
|
|
11
|
+
* // loader: return data({ ... }, { headers: TOKEN_PAGE_HEADERS });
|
|
12
|
+
*/
|
|
13
|
+
export declare function authLinks<L extends string>(links: AuthLinksOptions<L>): AuthLinks<L>;
|
|
14
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/session/links.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACX,SAAS,EACT,gBAAgB,EAGhB,MAAM,cAAc,CAAC;AA+DtB;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EACzC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GACxB,SAAS,CAAC,CAAC,CAAC,CAsBd"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SIGN_IN_PATHS, type SignInPaths, secondFactorPath, signInPath, } from './paths';
|
|
2
|
+
export { HOME, safeNext } from './safe-next';
|
|
3
|
+
export { type SecondFactorKind, type SignInStatus, type StepUpVia, TOTP_CODE, } from './second-factor';
|
|
4
|
+
export { INITIAL_STEP_UP, type StepUpAction, type StepUpPhase, type StepUpState, stepUpReducer, } from './step-up';
|
|
5
|
+
export type { StepUpConfirmAnswer, StepUpError, StepUpRequestAnswer, } from './step-up-answers';
|
|
6
|
+
export { type StepUpProps, stepUpProps } from './step-up-props';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/session/model/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,aAAa,EACb,KAAK,WAAW,EAChB,gBAAgB,EAChB,UAAU,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,SAAS,GACT,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,eAAe,EACf,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,aAAa,GACb,MAAM,WAAW,CAAC;AACnB,YAAY,EACX,mBAAmB,EACnB,WAAW,EACX,mBAAmB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOME2,
|
|
3
|
+
safeNext2,
|
|
4
|
+
SIGN_IN_PATHS2,
|
|
5
|
+
signInPath2,
|
|
6
|
+
secondFactorPath2,
|
|
7
|
+
TOTP_CODE2,
|
|
8
|
+
INITIAL_STEP_UP2,
|
|
9
|
+
stepUpReducer2,
|
|
10
|
+
stepUpProps2
|
|
11
|
+
} from "../../chunks/index-0tyw54ad.js";
|
|
12
|
+
export {
|
|
13
|
+
HOME2 as HOME,
|
|
14
|
+
INITIAL_STEP_UP2 as INITIAL_STEP_UP,
|
|
15
|
+
SIGN_IN_PATHS2 as SIGN_IN_PATHS,
|
|
16
|
+
TOTP_CODE2 as TOTP_CODE,
|
|
17
|
+
safeNext2 as safeNext,
|
|
18
|
+
secondFactorPath2 as secondFactorPath,
|
|
19
|
+
signInPath2 as signInPath,
|
|
20
|
+
stepUpProps2 as stepUpProps,
|
|
21
|
+
stepUpReducer2 as stepUpReducer
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//# debugId=6F702E5FEA8B922464756E2164756E21
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The two pages of a sign-in. */
|
|
2
|
+
export interface SignInPaths {
|
|
3
|
+
readonly signIn: string;
|
|
4
|
+
readonly secondFactor: string;
|
|
5
|
+
}
|
|
6
|
+
/** The defaults: `/sign-in` and `/sign-in/second-factor`. */
|
|
7
|
+
export declare const SIGN_IN_PATHS: SignInPaths;
|
|
8
|
+
/**
|
|
9
|
+
* The sign-in page's URL, coming back to `next` (none for the home page, and
|
|
10
|
+
* `next` goes through `safeNext`). `query` adds parameters, such as
|
|
11
|
+
* `{ expired: '1' }`; `path` replaces `/sign-in` and must be a path of this
|
|
12
|
+
* site with no query or fragment (a `TypeError` otherwise).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* signInPath('/orders'); // '/sign-in?next=%2Forders'
|
|
16
|
+
* signInPath('/', { query: { expired: '1' } }); // '/sign-in?expired=1'
|
|
17
|
+
*/
|
|
18
|
+
export declare function signInPath(next: string, options?: {
|
|
19
|
+
readonly query?: Record<string, string> | undefined;
|
|
20
|
+
readonly path?: string | undefined;
|
|
21
|
+
}): string;
|
|
22
|
+
/** The second-factor page's URL, coming back to `next` as `signInPath` does. */
|
|
23
|
+
export declare function secondFactorPath(next: string, path?: string): string;
|
|
24
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/session/model/paths.ts"],"names":[],"mappings":"AAGA,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED,6DAA6D;AAC7D,eAAO,MAAM,aAAa,EAAE,WAG3B,CAAC;AAcF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IACR,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B,GACJ,MAAM,CAMR;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAmC,GACvC,MAAM,CAER"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Where a sign-in lands when `next` is missing or unsafe. */
|
|
2
|
+
export declare const HOME = "/";
|
|
3
|
+
/**
|
|
4
|
+
* The `next` a sign-in may redirect to: a path of this site only. Anything
|
|
5
|
+
* that could leave it (`//host`, `/\host`, `https://…`, `javascript:…`, a
|
|
6
|
+
* control character a browser would strip, a value over 2000 characters)
|
|
7
|
+
* falls back to `/`. The path is read after normalisation, so `/a/..//host`
|
|
8
|
+
* is refused too.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* safeNext('/orders?page=2'); // '/orders?page=2'
|
|
12
|
+
* safeNext('//evil.test'); // '/'
|
|
13
|
+
*/
|
|
14
|
+
export declare function safeNext(value: string | null | undefined): string;
|
|
15
|
+
//# sourceMappingURL=safe-next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-next.d.ts","sourceRoot":"","sources":["../../../src/session/model/safe-next.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,eAAO,MAAM,IAAI,MAAM,CAAC;AAKxB;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAcjE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A code of an authenticator app: six digits. */
|
|
2
|
+
export declare const TOTP_CODE: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* What a second-factor form sends: `code` is the issued code (an app's, or
|
|
5
|
+
* the one mailed), `recovery` a recovery code. Janus has no name for it, only
|
|
6
|
+
* `confirm` and `recover`.
|
|
7
|
+
*/
|
|
8
|
+
export type SecondFactorKind = 'code' | 'recovery';
|
|
9
|
+
/** How a sign-in ended, in `@nxgt/janus`'s words (`SignInResult['status']`). */
|
|
10
|
+
export type SignInStatus = 'signedIn' | 'secondFactor';
|
|
11
|
+
/**
|
|
12
|
+
* How a step-up is confirmed, in `@nxgt/janus`'s words: a code mailed to the
|
|
13
|
+
* current address, or the account's second factor.
|
|
14
|
+
*/
|
|
15
|
+
export type StepUpVia = 'email' | 'secondFactor';
|
|
16
|
+
//# sourceMappingURL=second-factor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor.d.ts","sourceRoot":"","sources":["../../../src/session/model/second-factor.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,eAAO,MAAM,SAAS,EAAE,MAAkB,CAAC;AAE3C;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,UAAU,CAAC;AAEnD,gFAAgF;AAChF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,cAAc,CAAC;AAEvD;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A path of this site, or a `TypeError` naming `label`. With no `query`, the
|
|
3
|
+
* path holds no `?` or `#` either, and the message prints it only up to
|
|
4
|
+
* there, since what follows may be a token.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sitePath(path: string, { label, query, }: {
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly query?: boolean | undefined;
|
|
9
|
+
}): string;
|
|
10
|
+
//# sourceMappingURL=site-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-path.d.ts","sourceRoot":"","sources":["../../../src/session/model/site-path.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,QAAQ,CACvB,IAAI,EAAE,MAAM,EACZ,EACC,KAAK,EACL,KAAa,GACb,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAE,GACjE,MAAM,CAQR"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StepUpVia } from './second-factor';
|
|
2
|
+
/**
|
|
3
|
+
* Why a step-up step failed: a wrong code, a challenge gone, a mail held back
|
|
4
|
+
* for asking too often (`throttled`, with `retryAfter` when the API says how
|
|
5
|
+
* long), or anything else.
|
|
6
|
+
*/
|
|
7
|
+
export type StepUpError = 'invalid' | 'expired' | 'failed' | 'throttled';
|
|
8
|
+
/** What the server answers to a request for a challenge. */
|
|
9
|
+
export type StepUpRequestAnswer = {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly intent: 'request-step-up';
|
|
12
|
+
readonly challenge: string;
|
|
13
|
+
readonly via: StepUpVia;
|
|
14
|
+
} | {
|
|
15
|
+
readonly ok: false;
|
|
16
|
+
readonly intent: 'request-step-up';
|
|
17
|
+
readonly error: StepUpError;
|
|
18
|
+
/** The refusal worded for the page, in its language. */
|
|
19
|
+
readonly message?: string | undefined;
|
|
20
|
+
/** The seconds to wait before asking again (a whole number from 0), when `error` is `throttled` and the API says. */
|
|
21
|
+
readonly retryAfter?: number | undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* What the server answers to a code. `attemptsLeft` is the tries a refused
|
|
25
|
+
* code leaves; `locked` says the account's lockout is on, so asking again
|
|
26
|
+
* would fail too.
|
|
27
|
+
*/
|
|
28
|
+
export type StepUpConfirmAnswer = {
|
|
29
|
+
readonly ok: true;
|
|
30
|
+
readonly intent: 'confirm-step-up';
|
|
31
|
+
} | {
|
|
32
|
+
readonly ok: false;
|
|
33
|
+
readonly intent: 'confirm-step-up';
|
|
34
|
+
readonly error: StepUpError;
|
|
35
|
+
readonly attemptsLeft?: number | undefined;
|
|
36
|
+
readonly locked: boolean;
|
|
37
|
+
/** The refusal worded for the page, in its language. */
|
|
38
|
+
readonly message?: string | undefined;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=step-up-answers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"step-up-answers.d.ts","sourceRoot":"","sources":["../../../src/session/model/step-up-answers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEzE,4DAA4D;AAC5D,MAAM,MAAM,mBAAmB,GAC5B;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;CACvB,GACD;IACA,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,qHAAqH;IACrH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;AAEL;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,GACzD;IACA,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC,CAAC"}
|
|
@@ -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
|