@alxia/react-router 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +194 -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 +648 -0
- package/dist/session/index.js.map +23 -0
- package/dist/session/land.d.ts +28 -0
- package/dist/session/land.d.ts.map +1 -0
- package/dist/session/link-land.d.ts +20 -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 +128 -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 +18 -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 +34 -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 +1002 -0
- package/docs/roadmap.md +27 -0
- package/docs/troubleshooting.md +390 -1
- package/package.json +15 -2
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isApiErrorLike2,
|
|
3
|
+
retryAfterOf,
|
|
4
|
+
attemptsLeftOf2,
|
|
5
|
+
routeError2,
|
|
6
|
+
nextOf2,
|
|
7
|
+
hasSiteCheck,
|
|
8
|
+
refuseCrossSite
|
|
9
|
+
} from "../chunks/index-c3k47yw7.js";
|
|
10
|
+
import {
|
|
11
|
+
HOME2,
|
|
12
|
+
safeNext2,
|
|
13
|
+
sitePath,
|
|
14
|
+
SIGN_IN_PATHS2,
|
|
15
|
+
signInPath2,
|
|
16
|
+
secondFactorPath2
|
|
17
|
+
} from "../chunks/index-0tyw54ad.js";
|
|
18
|
+
|
|
19
|
+
// src/session/codes.ts
|
|
20
|
+
var INVALID_CODE = "CODE_INVALID";
|
|
21
|
+
var CHALLENGE_OVER = [
|
|
22
|
+
"TOKEN_UNKNOWN",
|
|
23
|
+
"TOKEN_SPENT",
|
|
24
|
+
"TOKEN_EXPIRED"
|
|
25
|
+
];
|
|
26
|
+
var MAIL_THROTTLED = "MAIL_THROTTLED";
|
|
27
|
+
// src/session/serialize.ts
|
|
28
|
+
var TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
29
|
+
var PATH = /^\/[^;\x00-\x1f\x7f]*$/;
|
|
30
|
+
var isCookieName = (name) => TOKEN.test(name);
|
|
31
|
+
var isCookiePath = (path) => PATH.test(path);
|
|
32
|
+
function serializeCookie(name, value, { maxAge, path = "/", secure }) {
|
|
33
|
+
if (!isCookieName(name)) {
|
|
34
|
+
throw new TypeError(`serializeCookie(): the name ${JSON.stringify(name)} is not a cookie name (an RFC 6265 token).`);
|
|
35
|
+
}
|
|
36
|
+
if (!isCookiePath(path)) {
|
|
37
|
+
throw new TypeError(`serializeCookie(): the path ${JSON.stringify(path)} is not a cookie path: it must start with "/" and hold no ";" or control character.`);
|
|
38
|
+
}
|
|
39
|
+
return [
|
|
40
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
41
|
+
`Max-Age=${Number.isFinite(maxAge) ? Math.max(0, Math.floor(maxAge)) : 0}`,
|
|
42
|
+
`Path=${path}`,
|
|
43
|
+
"HttpOnly",
|
|
44
|
+
...secure ? ["Secure"] : [],
|
|
45
|
+
"SameSite=Lax"
|
|
46
|
+
].join("; ");
|
|
47
|
+
}
|
|
48
|
+
function readCookie(request, name) {
|
|
49
|
+
for (const pair of request.headers.get("cookie")?.split(";") ?? []) {
|
|
50
|
+
const index = pair.indexOf("=");
|
|
51
|
+
if (index < 0 || pair.slice(0, index).trim() !== name)
|
|
52
|
+
continue;
|
|
53
|
+
const raw = pair.slice(index + 1).trim();
|
|
54
|
+
if (!raw)
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
return decodeURIComponent(raw);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function millisOf(at) {
|
|
65
|
+
const time = typeof at === "string" ? Date.parse(at) : at.getTime();
|
|
66
|
+
return Number.isNaN(time) ? 0 : time;
|
|
67
|
+
}
|
|
68
|
+
function secondsUntil(expiresAt, now = Date.now()) {
|
|
69
|
+
const end = millisOf(expiresAt);
|
|
70
|
+
return end === 0 ? 0 : Math.max(0, (end - now) / 1000);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/session/cookies.ts
|
|
74
|
+
var PREFIX = /^__(?:Host|Secure)-/;
|
|
75
|
+
function checkName(label, name) {
|
|
76
|
+
if (!isCookieName(name)) {
|
|
77
|
+
throw new TypeError(`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}".`);
|
|
78
|
+
}
|
|
79
|
+
if (PREFIX.test(name)) {
|
|
80
|
+
throw new TypeError(`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.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function sessionCookies(options) {
|
|
84
|
+
const {
|
|
85
|
+
secure,
|
|
86
|
+
challengePath = "/sign-in",
|
|
87
|
+
challengeSeconds = 5 * 60
|
|
88
|
+
} = options;
|
|
89
|
+
checkName("session", options.session);
|
|
90
|
+
checkName("challenge", options.challenge);
|
|
91
|
+
if (!Number.isInteger(challengeSeconds) || challengeSeconds <= 0) {
|
|
92
|
+
throw new TypeError(`sessionCookies(): challengeSeconds must be a positive integer; got ${String(challengeSeconds)}.`);
|
|
93
|
+
}
|
|
94
|
+
if (!isCookiePath(challengePath)) {
|
|
95
|
+
throw new TypeError(`sessionCookies(): challengePath must start with "/" and hold no ";" or control character; got ${JSON.stringify(challengePath)}.`);
|
|
96
|
+
}
|
|
97
|
+
const session = secure ? `__Host-${options.session}` : options.session;
|
|
98
|
+
const challenge = secure ? `__Secure-${options.challenge}` : options.challenge;
|
|
99
|
+
const clear = (name, path) => serializeCookie(name, "", { maxAge: 0, secure, ...path && { path } });
|
|
100
|
+
const read = (request) => {
|
|
101
|
+
const value = readCookie(request, session);
|
|
102
|
+
const dot = value?.indexOf(".") ?? -1;
|
|
103
|
+
if (!value || dot < 1)
|
|
104
|
+
return null;
|
|
105
|
+
const digits = value.slice(0, dot);
|
|
106
|
+
const expiresAt = Number(digits);
|
|
107
|
+
const token = value.slice(dot + 1);
|
|
108
|
+
return token && /^\d+$/.test(digits) && Number.isSafeInteger(expiresAt) ? { token, expiresAt } : null;
|
|
109
|
+
};
|
|
110
|
+
const commit = (token, expiresAt, now) => serializeCookie(session, `${millisOf(expiresAt)}.${token}`, {
|
|
111
|
+
maxAge: secondsUntil(expiresAt, now),
|
|
112
|
+
secure
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
names: { session, challenge },
|
|
116
|
+
read,
|
|
117
|
+
token: (request) => read(request)?.token ?? null,
|
|
118
|
+
commit,
|
|
119
|
+
destroy: () => clear(session),
|
|
120
|
+
refreshed(current, expiresAt) {
|
|
121
|
+
const end = expiresAt ? millisOf(expiresAt) : 0;
|
|
122
|
+
if (!expiresAt || end === 0 || end === current.expiresAt)
|
|
123
|
+
return {};
|
|
124
|
+
return { "Set-Cookie": commit(current.token, expiresAt) };
|
|
125
|
+
},
|
|
126
|
+
challengeOf: (request) => readCookie(request, challenge),
|
|
127
|
+
commitChallenge: (value, expiresAt, now) => serializeCookie(challenge, value, {
|
|
128
|
+
maxAge: Math.min(challengeSeconds, expiresAt ? secondsUntil(expiresAt, now) : challengeSeconds),
|
|
129
|
+
path: challengePath,
|
|
130
|
+
secure
|
|
131
|
+
}),
|
|
132
|
+
destroyChallenge: () => clear(challenge, challengePath),
|
|
133
|
+
signInPageHeaders(deadSession) {
|
|
134
|
+
const headers = new Headers;
|
|
135
|
+
headers.append("Set-Cookie", clear(challenge, challengePath));
|
|
136
|
+
if (deadSession)
|
|
137
|
+
headers.append("Set-Cookie", clear(session));
|
|
138
|
+
return headers;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// src/session/link-token.ts
|
|
143
|
+
import { data, replace } from "react-router";
|
|
144
|
+
|
|
145
|
+
// src/session/guard.ts
|
|
146
|
+
function samesitePost(json, request) {
|
|
147
|
+
json.requireJsonPost(request);
|
|
148
|
+
refuseCrossSite(json, request);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/session/link-token.ts
|
|
152
|
+
var TOKEN_PAGE_HEADERS = {
|
|
153
|
+
"Referrer-Policy": "no-referrer",
|
|
154
|
+
"X-Robots-Tag": "noindex, nofollow"
|
|
155
|
+
};
|
|
156
|
+
var pathOf = (path, call) => sitePath(path, { label: `${call}: a token page's path` });
|
|
157
|
+
var statePath = (path, state) => `${pathOf(path, "statePath()")}?${new URLSearchParams({ state })}`;
|
|
158
|
+
function withTokenHeaders(headers) {
|
|
159
|
+
const merged = new Headers(headers);
|
|
160
|
+
for (const [name, value] of Object.entries(TOKEN_PAGE_HEADERS)) {
|
|
161
|
+
merged.set(name, value);
|
|
162
|
+
}
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
var stateRedirect = (path, state, headers) => replace(statePath(path, state), { headers: withTokenHeaders(headers) });
|
|
166
|
+
var isTokenIssue = (issue) => {
|
|
167
|
+
const first = issue.path?.[0];
|
|
168
|
+
return (typeof first === "object" ? first.key : first) === "token";
|
|
169
|
+
};
|
|
170
|
+
async function linkArgs({ json }, invalidState, schema, request, path) {
|
|
171
|
+
samesitePost(json, request);
|
|
172
|
+
pathOf(path, "linkArgs()");
|
|
173
|
+
const body = await json.readJson(request);
|
|
174
|
+
const result = await schema["~standard"].validate(body);
|
|
175
|
+
if (!result.issues)
|
|
176
|
+
return result.value;
|
|
177
|
+
if (result.issues.some(isTokenIssue)) {
|
|
178
|
+
throw stateRedirect(path, invalidState);
|
|
179
|
+
}
|
|
180
|
+
throw data("Bad link request", { status: 400 });
|
|
181
|
+
}
|
|
182
|
+
async function spendToken(options, run, { request, language, path, states, fields: shown = [] }) {
|
|
183
|
+
const { errors } = options;
|
|
184
|
+
samesitePost(options.json, request);
|
|
185
|
+
pathOf(path, "spendToken()");
|
|
186
|
+
try {
|
|
187
|
+
return { ok: true, value: await run() };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const refusal = errors.apiRefusal(error, language);
|
|
190
|
+
if (!refusal)
|
|
191
|
+
throw error;
|
|
192
|
+
const { message, status } = refusal;
|
|
193
|
+
if (isApiErrorLike2(error)) {
|
|
194
|
+
const state = error.code ? states[error.code] : undefined;
|
|
195
|
+
if (state)
|
|
196
|
+
return { ok: false, response: stateRedirect(path, state) };
|
|
197
|
+
const fields = errors.fieldErrors(error, language);
|
|
198
|
+
const named = Object.keys(fields);
|
|
199
|
+
if (named.length && named.every((field) => shown.includes(field))) {
|
|
200
|
+
return { ok: false, refusal: { fieldErrors: fields }, status };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ok: false, refusal: { formError: message }, status };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
var answerOf = (spent) => ("response" in spent) ? spent.response : data(spent.refusal, { status: spent.status });
|
|
207
|
+
// src/session/links.ts
|
|
208
|
+
import { data as data3 } from "react-router";
|
|
209
|
+
|
|
210
|
+
// src/session/link-land.ts
|
|
211
|
+
import { data as data2, replace as replace2 } from "react-router";
|
|
212
|
+
|
|
213
|
+
// src/session/land.ts
|
|
214
|
+
import { redirect } from "react-router";
|
|
215
|
+
async function revokeSession({ options }, request, { context } = {}) {
|
|
216
|
+
samesitePost(options.json, request);
|
|
217
|
+
const token = options.cookies.token(request);
|
|
218
|
+
if (!token)
|
|
219
|
+
return false;
|
|
220
|
+
try {
|
|
221
|
+
await options.revoke(token, request, context);
|
|
222
|
+
} catch {}
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
var isLive = (answer) => answer.status === "signedIn" && Boolean(answer.token) && Boolean(answer.expiresAt) && millisOf(answer.expiresAt) > Date.now();
|
|
226
|
+
function landing({ options, paths }, answer, next) {
|
|
227
|
+
const { cookies } = options;
|
|
228
|
+
if (isLive(answer)) {
|
|
229
|
+
const headers = new Headers;
|
|
230
|
+
headers.append("Set-Cookie", cookies.commit(answer.token, answer.expiresAt));
|
|
231
|
+
headers.append("Set-Cookie", cookies.destroyChallenge());
|
|
232
|
+
for (const cookie of options.clear?.() ?? []) {
|
|
233
|
+
headers.append("Set-Cookie", cookie);
|
|
234
|
+
}
|
|
235
|
+
return redirect(safeNext2(next), { headers });
|
|
236
|
+
}
|
|
237
|
+
if (answer.status === "secondFactor" && answer.challenge) {
|
|
238
|
+
return redirect(secondFactorPath2(next, paths.secondFactor), {
|
|
239
|
+
headers: {
|
|
240
|
+
"Set-Cookie": cookies.commitChallenge(answer.challenge, answer.expiresAt)
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
async function landReplacing(flow, answer, next, {
|
|
247
|
+
request,
|
|
248
|
+
revokeOn = ["signedIn"],
|
|
249
|
+
context
|
|
250
|
+
}) {
|
|
251
|
+
samesitePost(flow.options.json, request);
|
|
252
|
+
const landed = landing(flow, answer, next);
|
|
253
|
+
if (!landed || answer.status === "other")
|
|
254
|
+
return landed;
|
|
255
|
+
if (!revokeOn.includes(answer.status))
|
|
256
|
+
return landed;
|
|
257
|
+
const hadSession = await revokeSession(flow, request, { context });
|
|
258
|
+
if (hadSession && answer.status !== "signedIn") {
|
|
259
|
+
landed.headers.append("Set-Cookie", flow.options.cookies.destroy());
|
|
260
|
+
}
|
|
261
|
+
return landed;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/session/link-land.ts
|
|
265
|
+
async function startOf({ start }, request, token) {
|
|
266
|
+
if (!start)
|
|
267
|
+
return HOME2;
|
|
268
|
+
try {
|
|
269
|
+
return safeNext2(await start(token, request));
|
|
270
|
+
} catch {
|
|
271
|
+
return HOME2;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function landByLink(options, answer, {
|
|
275
|
+
request,
|
|
276
|
+
language,
|
|
277
|
+
other,
|
|
278
|
+
context
|
|
279
|
+
}) {
|
|
280
|
+
samesitePost(options.json, request);
|
|
281
|
+
if (answer.status === "other" && other) {
|
|
282
|
+
return stateRedirect(other.path, other.state);
|
|
283
|
+
}
|
|
284
|
+
const next = isLive(answer) ? await startOf(options, request, answer.token) : HOME2;
|
|
285
|
+
const landed = await options.signIn.landReplacing(answer, next, {
|
|
286
|
+
request,
|
|
287
|
+
revokeOn: ["signedIn", "secondFactor"],
|
|
288
|
+
context
|
|
289
|
+
});
|
|
290
|
+
if (!landed) {
|
|
291
|
+
return data2({ formError: options.signIn.unavailable(language) }, { status: 503 });
|
|
292
|
+
}
|
|
293
|
+
const location = landed.headers.get("Location") ?? HOME2;
|
|
294
|
+
return replace2(location, { headers: withTokenHeaders(landed.headers) });
|
|
295
|
+
}
|
|
296
|
+
async function afterReset(options, resetDone, request, { context } = {}) {
|
|
297
|
+
samesitePost(options.json, request);
|
|
298
|
+
await options.signIn.revokeSession(request, { context });
|
|
299
|
+
const headers = withTokenHeaders();
|
|
300
|
+
headers.append("Set-Cookie", options.cookies.destroy());
|
|
301
|
+
headers.append("Set-Cookie", options.cookies.destroyChallenge());
|
|
302
|
+
for (const cookie of options.signIn.clear()) {
|
|
303
|
+
headers.append("Set-Cookie", cookie);
|
|
304
|
+
}
|
|
305
|
+
return replace2(resetDone, { headers });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/session/links.ts
|
|
309
|
+
var RESET_DONE = "/sign-in?password-reset=1";
|
|
310
|
+
var worded = ({ wording, errors }, error, language) => wording?.(error, language) ?? errors.apiErrorMessage(error, language);
|
|
311
|
+
async function requestLink(options, run, { request, language }) {
|
|
312
|
+
const { errors } = options;
|
|
313
|
+
samesitePost(options.json, request);
|
|
314
|
+
try {
|
|
315
|
+
await run();
|
|
316
|
+
return data3({ sent: true });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const api = isApiErrorLike2(error);
|
|
319
|
+
if (api && error.code === (options.codes?.mailThrottled ?? MAIL_THROTTLED)) {
|
|
320
|
+
const retryAfter = retryAfterOf(error);
|
|
321
|
+
return data3({
|
|
322
|
+
formError: worded(options, error, language),
|
|
323
|
+
throttled: true,
|
|
324
|
+
...retryAfter === undefined ? {} : { retryAfter }
|
|
325
|
+
}, { status: 429 });
|
|
326
|
+
}
|
|
327
|
+
if (api) {
|
|
328
|
+
const email = errors.fieldErrors(error, language)["email"];
|
|
329
|
+
if (email) {
|
|
330
|
+
return data3({ fieldErrors: { email } }, { status: error.status });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const refusal = errors.apiRefusal(error, language);
|
|
334
|
+
if (!refusal)
|
|
335
|
+
throw error;
|
|
336
|
+
return data3({ formError: api ? worded(options, error, language) : refusal.message }, { status: refusal.status });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function authLinks(links) {
|
|
340
|
+
const { signIn } = links;
|
|
341
|
+
const options = {
|
|
342
|
+
...links,
|
|
343
|
+
cookies: signIn.cookies,
|
|
344
|
+
errors: signIn.errors,
|
|
345
|
+
json: signIn.json
|
|
346
|
+
};
|
|
347
|
+
const resetDone = options.resetDone ?? RESET_DONE;
|
|
348
|
+
sitePath(resetDone, { label: "authLinks(): resetDone", query: true });
|
|
349
|
+
const invalid = options.invalidState ?? "invalid";
|
|
350
|
+
return {
|
|
351
|
+
statePath,
|
|
352
|
+
ended: stateRedirect,
|
|
353
|
+
linkArgs: (schema, request, path) => linkArgs(options, invalid, schema, request, path),
|
|
354
|
+
spendToken: (run, o) => spendToken(options, run, o),
|
|
355
|
+
answerOf,
|
|
356
|
+
landByLink: (answer, o) => landByLink(options, answer, o),
|
|
357
|
+
afterReset: (request, o) => afterReset(options, resetDone, request, o),
|
|
358
|
+
requestLink: (run, o) => requestLink(options, run, o)
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// src/session/second-factor.ts
|
|
362
|
+
import { data as data4 } from "react-router";
|
|
363
|
+
var MAX_CODE = 40;
|
|
364
|
+
function isMethod(value) {
|
|
365
|
+
return value === "code" || value === "recovery";
|
|
366
|
+
}
|
|
367
|
+
async function readCodeInput({ options }, request) {
|
|
368
|
+
samesitePost(options.json, request);
|
|
369
|
+
const body = await options.json.readJson(request);
|
|
370
|
+
const { kind, code } = body ?? {};
|
|
371
|
+
if (!isMethod(kind) || typeof code !== "string" || code.length > MAX_CODE) {
|
|
372
|
+
throw data4("Bad code request", { status: 400 });
|
|
373
|
+
}
|
|
374
|
+
return { kind, code };
|
|
375
|
+
}
|
|
376
|
+
function codeStep(input, challenge, steps) {
|
|
377
|
+
return input.kind === "code" ? steps.confirm(challenge, input.code) : steps.recover(challenge, input.code);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/session/step.ts
|
|
381
|
+
import { data as data5 } from "react-router";
|
|
382
|
+
function codeRefusal({ options, invalidCode, challengeOver }, error, language, whenNone) {
|
|
383
|
+
if (error.code !== invalidCode) {
|
|
384
|
+
return error.code && challengeOver.includes(error.code) ? undefined : options.errors.apiErrorMessage(error, language);
|
|
385
|
+
}
|
|
386
|
+
const message = options.errors.apiErrorMessage(error, language);
|
|
387
|
+
const count = attemptsLeftOf2(error);
|
|
388
|
+
if (count === undefined)
|
|
389
|
+
return message;
|
|
390
|
+
if (count === 0) {
|
|
391
|
+
return whenNone === undefined ? undefined : `${message} ${whenNone}`;
|
|
392
|
+
}
|
|
393
|
+
return `${message} ${options.attemptsLeft(count, language)}`;
|
|
394
|
+
}
|
|
395
|
+
var unavailableMessage = ({ unavailable, errors }, language) => unavailable?.(language) ?? errors.unavailableMessage(language);
|
|
396
|
+
async function signInStep(flow, run, {
|
|
397
|
+
request,
|
|
398
|
+
language,
|
|
399
|
+
next,
|
|
400
|
+
extra,
|
|
401
|
+
refused,
|
|
402
|
+
context
|
|
403
|
+
}) {
|
|
404
|
+
const { errors, json } = flow.options;
|
|
405
|
+
samesitePost(json, request);
|
|
406
|
+
const stay = (formError, status) => data5({ ...extra, formError }, { status });
|
|
407
|
+
const unreachable = () => stay(unavailableMessage(flow.options, language), 503);
|
|
408
|
+
let answer;
|
|
409
|
+
try {
|
|
410
|
+
answer = await run();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const refusal = errors.apiRefusal(error, language);
|
|
413
|
+
if (!refusal)
|
|
414
|
+
throw error;
|
|
415
|
+
if (!isApiErrorLike2(error))
|
|
416
|
+
return unreachable();
|
|
417
|
+
const own = refused?.(error);
|
|
418
|
+
if (own instanceof Response)
|
|
419
|
+
return own;
|
|
420
|
+
return stay(own ?? refusal.message, refusal.status);
|
|
421
|
+
}
|
|
422
|
+
return await landReplacing(flow, answer, next, { request, context }) ?? unreachable();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/session/viewer.ts
|
|
426
|
+
import { redirect as redirect2 } from "react-router";
|
|
427
|
+
var nextParam = (request) => safeNext2(new URL(request.url).searchParams.get("next"));
|
|
428
|
+
function signInRedirect({ options, paths }, request) {
|
|
429
|
+
const { endedParam, cookies } = options;
|
|
430
|
+
const query = endedParam && cookies.read(request) ? { [endedParam]: "1" } : undefined;
|
|
431
|
+
return redirect2(signInPath2(nextOf2(request.url), {
|
|
432
|
+
path: paths.signIn,
|
|
433
|
+
...query && { query }
|
|
434
|
+
}), { headers: { "Set-Cookie": cookies.destroy() } });
|
|
435
|
+
}
|
|
436
|
+
async function viewer({ options }, request, read, token = options.cookies.token(request)) {
|
|
437
|
+
if (!token)
|
|
438
|
+
return;
|
|
439
|
+
try {
|
|
440
|
+
return await read(token);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return isApiErrorLike2(error) && error.status === 401 ? null : undefined;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function requireViewer(context, request, read, expiresAt) {
|
|
446
|
+
const session = context.options.cookies.read(request);
|
|
447
|
+
if (!session)
|
|
448
|
+
throw signInRedirect(context, request);
|
|
449
|
+
let found;
|
|
450
|
+
try {
|
|
451
|
+
found = await read(session.token);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (isApiErrorLike2(error) && error.status === 401) {
|
|
454
|
+
throw signInRedirect(context, request);
|
|
455
|
+
}
|
|
456
|
+
throw routeError2(error);
|
|
457
|
+
}
|
|
458
|
+
if (!found)
|
|
459
|
+
throw signInRedirect(context, request);
|
|
460
|
+
return {
|
|
461
|
+
viewer: found,
|
|
462
|
+
headers: context.options.cookies.refreshed(session, expiresAt(found))
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async function signOut(flow, request, {
|
|
466
|
+
to,
|
|
467
|
+
context
|
|
468
|
+
} = {}) {
|
|
469
|
+
const { options, paths } = flow;
|
|
470
|
+
await options.json.readJson(request);
|
|
471
|
+
await revokeSession(flow, request, { context });
|
|
472
|
+
const headers = new Headers;
|
|
473
|
+
headers.append("Set-Cookie", options.cookies.destroy());
|
|
474
|
+
headers.append("Set-Cookie", options.cookies.destroyChallenge());
|
|
475
|
+
for (const cookie of options.clear?.() ?? []) {
|
|
476
|
+
headers.append("Set-Cookie", cookie);
|
|
477
|
+
}
|
|
478
|
+
return redirect2(to === undefined ? `${paths.signIn}?signed-out=1` : safeNext2(to), { headers });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/session/sign-in.ts
|
|
482
|
+
function resolve(options) {
|
|
483
|
+
const paths = { ...SIGN_IN_PATHS2, ...options.paths };
|
|
484
|
+
for (const key of ["signIn", "secondFactor"]) {
|
|
485
|
+
sitePath(paths[key], { label: `signInFlow(): paths.${key}` });
|
|
486
|
+
}
|
|
487
|
+
if (!hasSiteCheck(options.json)) {
|
|
488
|
+
throw new TypeError("signInFlow(): json must come from jsonActions(), which carries the site check every step runs; this object has none (a hand-made double?). Pass the app's jsonActions({ origin }) instance, or a spread copy of it.");
|
|
489
|
+
}
|
|
490
|
+
const invalidCode = options.codes?.invalidCode ?? INVALID_CODE;
|
|
491
|
+
const challengeOver = options.codes?.challengeOver ?? CHALLENGE_OVER;
|
|
492
|
+
if (challengeOver.includes(invalidCode)) {
|
|
493
|
+
throw new TypeError(`signInFlow(): the code ${JSON.stringify(invalidCode)} is both the wrong-code code (codes.invalidCode) and in codes.challengeOver. A wrong code that leaves tries cannot end the challenge.`);
|
|
494
|
+
}
|
|
495
|
+
return { options, paths, invalidCode, challengeOver };
|
|
496
|
+
}
|
|
497
|
+
function signInFlow(options) {
|
|
498
|
+
const context = resolve(options);
|
|
499
|
+
const { options: o } = context;
|
|
500
|
+
return {
|
|
501
|
+
cookies: o.cookies,
|
|
502
|
+
errors: o.errors,
|
|
503
|
+
json: o.json,
|
|
504
|
+
unavailable: (language) => unavailableMessage(o, language),
|
|
505
|
+
clear: () => o.clear?.() ?? [],
|
|
506
|
+
attemptsLeft: o.attemptsLeft,
|
|
507
|
+
guard: (request) => samesitePost(o.json, request),
|
|
508
|
+
nextParam,
|
|
509
|
+
signInRedirect: (request) => signInRedirect(context, request),
|
|
510
|
+
landing: (answer, next) => landing(context, answer, next),
|
|
511
|
+
landReplacing: (answer, next, o) => landReplacing(context, answer, next, o),
|
|
512
|
+
signInStep: (run, o) => signInStep(context, run, o),
|
|
513
|
+
codeRefusal: (error, language, whenNone) => codeRefusal(context, error, language, whenNone),
|
|
514
|
+
readCodeInput: (request) => readCodeInput(context, request),
|
|
515
|
+
codeStep,
|
|
516
|
+
revokeSession: (request, o) => revokeSession(context, request, o),
|
|
517
|
+
signOut: (request, o) => signOut(context, request, o),
|
|
518
|
+
viewer: (request, read, token) => viewer(context, request, read, token),
|
|
519
|
+
requireViewer: (request, read, expiresAt) => requireViewer(context, request, read, expiresAt)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
// src/session/step-up.ts
|
|
523
|
+
import { data as data6 } from "react-router";
|
|
524
|
+
|
|
525
|
+
// src/session/step-up-codes.ts
|
|
526
|
+
var STEP_UP_REQUIRED = "STEP_UP_REQUIRED";
|
|
527
|
+
var STEP_UP_OVER = [
|
|
528
|
+
...CHALLENGE_OVER,
|
|
529
|
+
"TOKEN_STALE",
|
|
530
|
+
"SECOND_FACTOR_ACTIVE",
|
|
531
|
+
"SECOND_FACTOR_NOT_ENROLLED"
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
// src/session/step-up.ts
|
|
535
|
+
function resolve2(steps) {
|
|
536
|
+
const invalidCode = steps.codes?.invalidCode ?? INVALID_CODE;
|
|
537
|
+
const challengeOver = steps.codes?.challengeOver ?? STEP_UP_OVER;
|
|
538
|
+
if (challengeOver.includes(invalidCode)) {
|
|
539
|
+
throw new TypeError(`stepUpSteps(): the code ${JSON.stringify(invalidCode)} is both the wrong-code code (codes.invalidCode) and in codes.challengeOver. A wrong code that leaves tries cannot end the challenge.`);
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
options: steps.signIn,
|
|
543
|
+
steps,
|
|
544
|
+
invalidCode,
|
|
545
|
+
challengeOver,
|
|
546
|
+
stepUpRequired: steps.codes?.stepUpRequired ?? STEP_UP_REQUIRED,
|
|
547
|
+
mailThrottled: steps.codes?.mailThrottled ?? MAIL_THROTTLED
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function worded2({ steps }, error, language) {
|
|
551
|
+
return steps.wording?.(error, language) ?? steps.signIn.errors.apiErrorMessage(error, language);
|
|
552
|
+
}
|
|
553
|
+
function refusedCode(context, error, via, language) {
|
|
554
|
+
const { steps } = context;
|
|
555
|
+
const attemptsLeft = attemptsLeftOf2(error);
|
|
556
|
+
if (attemptsLeft === 0 && via !== "secondFactor") {
|
|
557
|
+
return {
|
|
558
|
+
error: "expired",
|
|
559
|
+
attemptsLeft,
|
|
560
|
+
locked: false,
|
|
561
|
+
message: steps.spent(language)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const locked = attemptsLeft === 0;
|
|
565
|
+
const message = codeRefusal(context, error, language, locked ? steps.locked?.(language) : undefined) ?? steps.signIn.errors.apiErrorMessage(error, language);
|
|
566
|
+
return {
|
|
567
|
+
error: "invalid",
|
|
568
|
+
...attemptsLeft === undefined ? {} : { attemptsLeft },
|
|
569
|
+
locked,
|
|
570
|
+
message
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function requestStep(context, run, { request, language }) {
|
|
574
|
+
samesitePost(context.steps.signIn.json, request);
|
|
575
|
+
const intent = "request-step-up";
|
|
576
|
+
try {
|
|
577
|
+
const { challenge, via } = await run();
|
|
578
|
+
return { ok: true, intent, challenge, via };
|
|
579
|
+
} catch (error) {
|
|
580
|
+
const refusal = context.steps.signIn.errors.apiRefusal(error, language);
|
|
581
|
+
if (!refusal)
|
|
582
|
+
throw error;
|
|
583
|
+
const api = isApiErrorLike2(error);
|
|
584
|
+
const message = api ? worded2(context, error, language) : refusal.message;
|
|
585
|
+
const throttled = api && error.code === context.mailThrottled;
|
|
586
|
+
const retryAfter = throttled ? retryAfterOf(error) : undefined;
|
|
587
|
+
return data6({
|
|
588
|
+
ok: false,
|
|
589
|
+
intent,
|
|
590
|
+
error: throttled ? "throttled" : "failed",
|
|
591
|
+
message,
|
|
592
|
+
...retryAfter === undefined ? {} : { retryAfter }
|
|
593
|
+
}, { status: refusal.status });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function confirmStep(context, run, { request, language, via }) {
|
|
597
|
+
samesitePost(context.steps.signIn.json, request);
|
|
598
|
+
const intent = "confirm-step-up";
|
|
599
|
+
try {
|
|
600
|
+
await run();
|
|
601
|
+
return { ok: true, intent };
|
|
602
|
+
} catch (error) {
|
|
603
|
+
const refusal = context.steps.signIn.errors.apiRefusal(error, language);
|
|
604
|
+
if (!refusal)
|
|
605
|
+
throw error;
|
|
606
|
+
const stay = (failure) => data6({ ok: false, intent, ...failure }, { status: refusal.status });
|
|
607
|
+
const failed = (message) => stay({ error: "failed", locked: false, message });
|
|
608
|
+
if (!isApiErrorLike2(error))
|
|
609
|
+
return failed(refusal.message);
|
|
610
|
+
if (error.code === context.invalidCode) {
|
|
611
|
+
return stay(refusedCode(context, error, via, language));
|
|
612
|
+
}
|
|
613
|
+
if (error.code && context.challengeOver.includes(error.code)) {
|
|
614
|
+
return stay({
|
|
615
|
+
error: "expired",
|
|
616
|
+
locked: false,
|
|
617
|
+
message: context.steps.spent(language)
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return failed(worded2(context, error, language));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function stepUpSteps(options) {
|
|
624
|
+
const context = resolve2(options);
|
|
625
|
+
return {
|
|
626
|
+
request: (run, o) => requestStep(context, run, o),
|
|
627
|
+
confirm: (run, o) => confirmStep(context, run, o),
|
|
628
|
+
isStepUpRequired: (error) => isApiErrorLike2(error) && error.code === context.stepUpRequired
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
export {
|
|
632
|
+
CHALLENGE_OVER,
|
|
633
|
+
INVALID_CODE,
|
|
634
|
+
MAIL_THROTTLED,
|
|
635
|
+
STEP_UP_OVER,
|
|
636
|
+
STEP_UP_REQUIRED,
|
|
637
|
+
TOKEN_PAGE_HEADERS,
|
|
638
|
+
authLinks,
|
|
639
|
+
readCookie,
|
|
640
|
+
secondsUntil,
|
|
641
|
+
serializeCookie,
|
|
642
|
+
sessionCookies,
|
|
643
|
+
signInFlow,
|
|
644
|
+
stepUpSteps
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
//# debugId=A957A7CA74458AAE64756E2164756E21
|
|
648
|
+
//# sourceMappingURL=index.js.map
|