@softr/sdk 0.0.24 → 0.0.25
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/dist/assertions.js +1 -7
- package/dist/auth/methods/authenticator.d.ts +80 -0
- package/dist/auth/methods/authenticator.js +175 -0
- package/dist/auth/methods/code.d.ts +40 -0
- package/dist/auth/methods/code.js +74 -0
- package/dist/auth/methods/magicLink.d.ts +24 -0
- package/dist/auth/methods/magicLink.js +53 -0
- package/dist/auth/methods/password.d.ts +19 -5
- package/dist/auth/methods/password.js +36 -15
- package/dist/auth/methods/signup.d.ts +22 -14
- package/dist/auth/methods/signup.js +48 -54
- package/dist/auth/methods/social.d.ts +35 -0
- package/dist/auth/methods/social.js +58 -0
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/platform.js +185 -7
- package/dist/auth/server.d.ts +15 -0
- package/dist/auth/server.js +16 -1
- package/dist/auth/session.js +35 -2
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +2 -2
- package/skills/softr-sdk/auth/SKILL.md +222 -36
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/data-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/reading-records/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
package/dist/assertions.js
CHANGED
|
@@ -18,9 +18,6 @@ function assert(condition, message) {
|
|
|
18
18
|
function panic(message) {
|
|
19
19
|
throwAtCaller(panic, new AssertionError(unwrapErrorMessage(message)));
|
|
20
20
|
}
|
|
21
|
-
function todo(message) {
|
|
22
|
-
throwAtCaller(todo, new UnimplementedError(unwrapErrorMessage(message)));
|
|
23
|
-
}
|
|
24
21
|
/**
|
|
25
22
|
* A soft assertion that throws an error in non prod environments, including
|
|
26
23
|
* staging, but logs an error in production mode.
|
|
@@ -47,9 +44,6 @@ var AssertionError = class extends Error {
|
|
|
47
44
|
var SoftAssertionError = class extends Error {
|
|
48
45
|
name = "SoftAssertionError";
|
|
49
46
|
};
|
|
50
|
-
var UnimplementedError = class extends Error {
|
|
51
|
-
name = "UnimplementedError";
|
|
52
|
-
};
|
|
53
47
|
function unwrapErrorMessage(message) {
|
|
54
48
|
return typeof message === "function" ? message() : message ?? "";
|
|
55
49
|
}
|
|
@@ -65,4 +59,4 @@ function throwAtCaller(f, ex) {
|
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
//#endregion
|
|
68
|
-
export { assert, panic, requireDefined, softAssert
|
|
62
|
+
export { assert, panic, requireDefined, softAssert };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { User } from "../user.js";
|
|
2
|
+
import { AuthError, Result } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/methods/authenticator.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Starts enrolling an authenticator app, for a sign-in that asked to set one up
|
|
8
|
+
* or for a signed-in visitor adding one.
|
|
9
|
+
*
|
|
10
|
+
* `otpauthUri` is what an authenticator app reads. `qrPngBase64` is that same
|
|
11
|
+
* URI as a PNG for `<img src="data:image/png;base64,…">`, so a page can show
|
|
12
|
+
* the QR code without a QR library. Neither one enrols anybody.
|
|
13
|
+
* {@link confirmAuthenticatorSetup} does that, once it accepts a first code.
|
|
14
|
+
*
|
|
15
|
+
* Call it again to hand back a fresh QR code, for a visitor who lost the first
|
|
16
|
+
* one. Only the newest one enrols them.
|
|
17
|
+
*/
|
|
18
|
+
declare function startAuthenticatorSetup(): Promise<Result<{
|
|
19
|
+
otpauthUri: string;
|
|
20
|
+
qrPngBase64: string;
|
|
21
|
+
}, AuthError>>;
|
|
22
|
+
/**
|
|
23
|
+
* Enrols the authenticator app on its first code and signs the visitor in.
|
|
24
|
+
*
|
|
25
|
+
* This is the only call that returns the backup codes, and it returns them
|
|
26
|
+
* once. Nothing fetches them again, so render them before leaving the page.
|
|
27
|
+
*/
|
|
28
|
+
declare function confirmAuthenticatorSetup(details: {
|
|
29
|
+
code: string;
|
|
30
|
+
}): Promise<Result<{
|
|
31
|
+
user: User;
|
|
32
|
+
backupCodes: Array<string>;
|
|
33
|
+
}, AuthError>>;
|
|
34
|
+
/**
|
|
35
|
+
* Skips enrolment and finishes the sign-in, for a visitor whose app offers an
|
|
36
|
+
* authenticator without requiring one. Only the `setupOptional` step.
|
|
37
|
+
*
|
|
38
|
+
* Later sign-ins stop asking.
|
|
39
|
+
*/
|
|
40
|
+
declare function skipAuthenticatorSetup(): Promise<Result<User, AuthError>>;
|
|
41
|
+
/**
|
|
42
|
+
* Finishes a sign-in that stopped at the `verify` step, with the code the
|
|
43
|
+
* visitor's authenticator app shows or one of their backup codes.
|
|
44
|
+
*
|
|
45
|
+
* Each backup code works once.
|
|
46
|
+
*/
|
|
47
|
+
declare function verifyAuthenticator(credentials: {
|
|
48
|
+
code: string;
|
|
49
|
+
} | {
|
|
50
|
+
backupCode: string;
|
|
51
|
+
}): Promise<Result<User, AuthError>>;
|
|
52
|
+
/**
|
|
53
|
+
* Emails the signed-in visitor the code that {@link resetAuthenticator} and
|
|
54
|
+
* {@link removeAuthenticator} ask for.
|
|
55
|
+
*
|
|
56
|
+
* Both change how the visitor signs in, so both want a code from their inbox
|
|
57
|
+
* on top of being signed in.
|
|
58
|
+
*/
|
|
59
|
+
declare function sendAuthenticatorResetCode(): Promise<Result<null, AuthError>>;
|
|
60
|
+
/**
|
|
61
|
+
* Unpairs the visitor's authenticator app so they can enrol a new one with
|
|
62
|
+
* {@link startAuthenticatorSetup}, after a lost or replaced phone.
|
|
63
|
+
*
|
|
64
|
+
* `code` is the one {@link sendAuthenticatorResetCode} emailed.
|
|
65
|
+
*/
|
|
66
|
+
declare function resetAuthenticator(details: {
|
|
67
|
+
code: string;
|
|
68
|
+
}): Promise<Result<null, AuthError>>;
|
|
69
|
+
/**
|
|
70
|
+
* Turns the visitor's authenticator app off entirely.
|
|
71
|
+
*
|
|
72
|
+
* `code` is the one {@link sendAuthenticatorResetCode} emailed. An app that
|
|
73
|
+
* requires an authenticator will ask them to set one up again on their next
|
|
74
|
+
* sign-in.
|
|
75
|
+
*/
|
|
76
|
+
declare function removeAuthenticator(details: {
|
|
77
|
+
code: string;
|
|
78
|
+
}): Promise<Result<null, AuthError>>;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { assert, panic } from "../../assertions.js";
|
|
2
|
+
import { writeCookies } from "../../cookies.js";
|
|
3
|
+
import { sessionConfig } from "../config.js";
|
|
4
|
+
import { makePlatform } from "../platform.js";
|
|
5
|
+
import { clearPendingAuth, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken, readSessionToken } from "../session.js";
|
|
6
|
+
import { authError } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
//#region src/auth/methods/authenticator.ts
|
|
9
|
+
/**
|
|
10
|
+
* Starts enrolling an authenticator app, for a sign-in that asked to set one up
|
|
11
|
+
* or for a signed-in visitor adding one.
|
|
12
|
+
*
|
|
13
|
+
* `otpauthUri` is what an authenticator app reads. `qrPngBase64` is that same
|
|
14
|
+
* URI as a PNG for `<img src="data:image/png;base64,…">`, so a page can show
|
|
15
|
+
* the QR code without a QR library. Neither one enrols anybody.
|
|
16
|
+
* {@link confirmAuthenticatorSetup} does that, once it accepts a first code.
|
|
17
|
+
*
|
|
18
|
+
* Call it again to hand back a fresh QR code, for a visitor who lost the first
|
|
19
|
+
* one. Only the newest one enrols them.
|
|
20
|
+
*/
|
|
21
|
+
async function startAuthenticatorSetup() {
|
|
22
|
+
const result = await makePlatform(sessionConfig().applicationId).authenticatorSetupInit(readPendingAuthToken() ?? readSessionToken());
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
if (result.status === 401) return authError("notAuthenticated", result.message);
|
|
25
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
26
|
+
if (result.status >= 500) panic(`Expected authenticator setup to start, but got ${result.status}: ` + result.message);
|
|
27
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
data: result.data
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Enrols the authenticator app on its first code and signs the visitor in.
|
|
36
|
+
*
|
|
37
|
+
* This is the only call that returns the backup codes, and it returns them
|
|
38
|
+
* once. Nothing fetches them again, so render them before leaving the page.
|
|
39
|
+
*/
|
|
40
|
+
async function confirmAuthenticatorSetup(details) {
|
|
41
|
+
const result = await makePlatform(sessionConfig().applicationId).authenticatorSetupComplete(readPendingAuthToken() ?? readSessionToken(), details.code);
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
|
|
44
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
45
|
+
if (result.status >= 500) panic(`Expected authenticator setup to finish, but got ${result.status}: ` + result.message);
|
|
46
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
|
|
47
|
+
}
|
|
48
|
+
const { session, backupCodes } = result.data;
|
|
49
|
+
writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
|
|
50
|
+
user: session.user,
|
|
51
|
+
permissions: session.permissions
|
|
52
|
+
}, session.jwt)]);
|
|
53
|
+
clearPendingAuth();
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
data: {
|
|
57
|
+
user: session.user,
|
|
58
|
+
backupCodes
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Skips enrolment and finishes the sign-in, for a visitor whose app offers an
|
|
64
|
+
* authenticator without requiring one. Only the `setupOptional` step.
|
|
65
|
+
*
|
|
66
|
+
* Later sign-ins stop asking.
|
|
67
|
+
*/
|
|
68
|
+
async function skipAuthenticatorSetup() {
|
|
69
|
+
const result = await makePlatform(sessionConfig().applicationId).authenticatorSetupDismiss(readPendingAuthToken() ?? readSessionToken());
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
if (result.status === 401) return authError("notAuthenticated", result.message);
|
|
72
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
73
|
+
if (result.status >= 500) panic(`Expected to skip authenticator setup, but got ${result.status}: ` + result.message);
|
|
74
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
|
|
75
|
+
}
|
|
76
|
+
const { session } = result.data;
|
|
77
|
+
assert(session, "Expected a session from skipping setup, but got none");
|
|
78
|
+
writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
|
|
79
|
+
user: session.user,
|
|
80
|
+
permissions: session.permissions
|
|
81
|
+
}, session.jwt)]);
|
|
82
|
+
clearPendingAuth();
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
data: session.user
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Finishes a sign-in that stopped at the `verify` step, with the code the
|
|
90
|
+
* visitor's authenticator app shows or one of their backup codes.
|
|
91
|
+
*
|
|
92
|
+
* Each backup code works once.
|
|
93
|
+
*/
|
|
94
|
+
async function verifyAuthenticator(credentials) {
|
|
95
|
+
const result = await makePlatform(sessionConfig().applicationId).authenticatorVerify(readPendingAuthToken() ?? readSessionToken(), "code" in credentials ? { totpCode: credentials.code } : { backupCode: credentials.backupCode });
|
|
96
|
+
if (!result.ok) {
|
|
97
|
+
if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
|
|
98
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
99
|
+
if (result.status >= 500) panic(`Expected to verify the authenticator, but got ${result.status}: ` + result.message);
|
|
100
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
|
|
101
|
+
}
|
|
102
|
+
const { session } = result.data;
|
|
103
|
+
assert(session, "Expected a session from verification, but got none");
|
|
104
|
+
writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
|
|
105
|
+
user: session.user,
|
|
106
|
+
permissions: session.permissions
|
|
107
|
+
}, session.jwt)]);
|
|
108
|
+
clearPendingAuth();
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
data: session.user
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Emails the signed-in visitor the code that {@link resetAuthenticator} and
|
|
116
|
+
* {@link removeAuthenticator} ask for.
|
|
117
|
+
*
|
|
118
|
+
* Both change how the visitor signs in, so both want a code from their inbox
|
|
119
|
+
* on top of being signed in.
|
|
120
|
+
*/
|
|
121
|
+
async function sendAuthenticatorResetCode() {
|
|
122
|
+
const result = await makePlatform(sessionConfig().applicationId).authenticatorSendResetCode(readSessionToken());
|
|
123
|
+
if (!result.ok) {
|
|
124
|
+
if (result.status === 401) return authError("notAuthenticated", result.message);
|
|
125
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
126
|
+
if (result.status >= 500) panic(`Expected the reset code to be emailed, but got ${result.status}: ` + result.message);
|
|
127
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
data: null
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Unpairs the visitor's authenticator app so they can enrol a new one with
|
|
136
|
+
* {@link startAuthenticatorSetup}, after a lost or replaced phone.
|
|
137
|
+
*
|
|
138
|
+
* `code` is the one {@link sendAuthenticatorResetCode} emailed.
|
|
139
|
+
*/
|
|
140
|
+
async function resetAuthenticator(details) {
|
|
141
|
+
const result = await makePlatform(sessionConfig().applicationId).authenticatorReset(readSessionToken(), details.code);
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
|
|
144
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
145
|
+
if (result.status >= 500) panic(`Expected the authenticator to be reset, but got ${result.status}: ` + result.message);
|
|
146
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
data: null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Turns the visitor's authenticator app off entirely.
|
|
155
|
+
*
|
|
156
|
+
* `code` is the one {@link sendAuthenticatorResetCode} emailed. An app that
|
|
157
|
+
* requires an authenticator will ask them to set one up again on their next
|
|
158
|
+
* sign-in.
|
|
159
|
+
*/
|
|
160
|
+
async function removeAuthenticator(details) {
|
|
161
|
+
const result = await makePlatform(sessionConfig().applicationId).authenticatorRemove(readSessionToken(), details.code);
|
|
162
|
+
if (!result.ok) {
|
|
163
|
+
if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
|
|
164
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
165
|
+
if (result.status >= 500) panic(`Expected the authenticator to be removed, but got ${result.status}: ` + result.message);
|
|
166
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
data: null
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
export { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { User } from "../user.js";
|
|
2
|
+
import { AuthError, Result } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/methods/code.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emails a one-time sign-in code, for an app that signs people in with a code
|
|
8
|
+
* rather than a password.
|
|
9
|
+
*
|
|
10
|
+
* It succeeds whether or not the address has an account, so nobody can use the
|
|
11
|
+
* form it backs to find out who is registered. Say "check your email" either
|
|
12
|
+
* way.
|
|
13
|
+
*/
|
|
14
|
+
declare function sendSignInCode(details: {
|
|
15
|
+
email: string;
|
|
16
|
+
}): Promise<Result<null, AuthError>>;
|
|
17
|
+
/**
|
|
18
|
+
* Signs a visitor in with the code {@link sendSignInCode} emailed them.
|
|
19
|
+
*
|
|
20
|
+
* A `signedIn` result means the visitor is signed in. An app that also asks for
|
|
21
|
+
* an authenticator app answers `authenticatorRequired` instead, naming the step
|
|
22
|
+
* to send them to next.
|
|
23
|
+
*/
|
|
24
|
+
declare function signInWithCode(credentials: {
|
|
25
|
+
email: string;
|
|
26
|
+
code: string;
|
|
27
|
+
}): Promise<Result<{
|
|
28
|
+
status: "signedIn";
|
|
29
|
+
user: User;
|
|
30
|
+
} | {
|
|
31
|
+
status: "authenticatorRequired";
|
|
32
|
+
/**
|
|
33
|
+
* `verify`: they have an authenticator app and owe a code.
|
|
34
|
+
* `setup`: the app requires one and they have none yet.
|
|
35
|
+
* `setupOptional`: same, except they may skip it instead.
|
|
36
|
+
*/
|
|
37
|
+
step: "verify" | "setup" | "setupOptional";
|
|
38
|
+
}, AuthError>>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { sendSignInCode, signInWithCode };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { assert, panic } from "../../assertions.js";
|
|
2
|
+
import { writeCookies } from "../../cookies.js";
|
|
3
|
+
import { sessionConfig } from "../config.js";
|
|
4
|
+
import { makePlatform } from "../platform.js";
|
|
5
|
+
import { clearPendingAuth, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
|
|
6
|
+
import { authError } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
//#region src/auth/methods/code.ts
|
|
9
|
+
/**
|
|
10
|
+
* Emails a one-time sign-in code, for an app that signs people in with a code
|
|
11
|
+
* rather than a password.
|
|
12
|
+
*
|
|
13
|
+
* It succeeds whether or not the address has an account, so nobody can use the
|
|
14
|
+
* form it backs to find out who is registered. Say "check your email" either
|
|
15
|
+
* way.
|
|
16
|
+
*/
|
|
17
|
+
async function sendSignInCode(details) {
|
|
18
|
+
const result = await makePlatform(sessionConfig().applicationId).sendSignInCode(details.email);
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
21
|
+
if (result.status === 400) return authError("invalidDetails", result.message);
|
|
22
|
+
if (result.status >= 500) panic(`Expected the sign-in code to be emailed, but got ${result.status}: ` + result.message);
|
|
23
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app signs people in with an emailed code.`);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
success: true,
|
|
27
|
+
data: null
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Signs a visitor in with the code {@link sendSignInCode} emailed them.
|
|
32
|
+
*
|
|
33
|
+
* A `signedIn` result means the visitor is signed in. An app that also asks for
|
|
34
|
+
* an authenticator app answers `authenticatorRequired` instead, naming the step
|
|
35
|
+
* to send them to next.
|
|
36
|
+
*/
|
|
37
|
+
async function signInWithCode(credentials) {
|
|
38
|
+
const result = await makePlatform(sessionConfig().applicationId).signInWithCode(credentials);
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
41
|
+
if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
|
|
42
|
+
if (result.status >= 500) panic(`Expected sign-in to succeed, but got ${result.status}: ` + result.message);
|
|
43
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app signs people in with an emailed code.`);
|
|
44
|
+
}
|
|
45
|
+
if (result.data.status === "MFA_REQUIRED") {
|
|
46
|
+
const { mfa } = result.data;
|
|
47
|
+
assert(mfa, "Expected a challenge from sign-in, but got none");
|
|
48
|
+
writeCookies([makePendingAuthCookie(mfa.challengeToken)]);
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
data: {
|
|
52
|
+
status: "authenticatorRequired",
|
|
53
|
+
step: mfa.action === "VERIFICATION_REQUIRED" ? "verify" : mfa.action === "SETUP_REQUIRED" ? "setup" : "setupOptional"
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const { session } = result.data;
|
|
58
|
+
assert(session, "Expected a session from sign-in, but got none");
|
|
59
|
+
writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
|
|
60
|
+
user: session.user,
|
|
61
|
+
permissions: session.permissions
|
|
62
|
+
}, session.jwt)]);
|
|
63
|
+
clearPendingAuth();
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
data: {
|
|
67
|
+
status: "signedIn",
|
|
68
|
+
user: session.user
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { sendSignInCode, signInWithCode };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { User } from "../user.js";
|
|
2
|
+
import { AuthError, Result } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/methods/magicLink.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Signs a visitor in with the token from a magic link they followed.
|
|
8
|
+
*
|
|
9
|
+
* The token keeps working after it is used, so anyone holding the link can sign
|
|
10
|
+
* in as that visitor. Never log it, and redirect away from the URL that carried
|
|
11
|
+
* it.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
declare function signInWithMagicLink(credentials: {
|
|
15
|
+
token: string;
|
|
16
|
+
}): Promise<Result<{
|
|
17
|
+
status: "signedIn";
|
|
18
|
+
user: User;
|
|
19
|
+
} | {
|
|
20
|
+
status: "authenticatorRequired";
|
|
21
|
+
step: "verify" | "setup" | "setupOptional";
|
|
22
|
+
}, AuthError>>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { signInWithMagicLink };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { assert, panic } from "../../assertions.js";
|
|
2
|
+
import { writeCookies } from "../../cookies.js";
|
|
3
|
+
import { sessionConfig } from "../config.js";
|
|
4
|
+
import { makePlatform } from "../platform.js";
|
|
5
|
+
import { makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
|
|
6
|
+
import { authError } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
//#region src/auth/methods/magicLink.ts
|
|
9
|
+
/**
|
|
10
|
+
* Signs a visitor in with the token from a magic link they followed.
|
|
11
|
+
*
|
|
12
|
+
* The token keeps working after it is used, so anyone holding the link can sign
|
|
13
|
+
* in as that visitor. Never log it, and redirect away from the URL that carried
|
|
14
|
+
* it.
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
async function signInWithMagicLink(credentials) {
|
|
18
|
+
const result = await makePlatform(sessionConfig().applicationId).signInWithMagicLink(credentials);
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
21
|
+
if (result.status === 400 || result.status === 401) return authError("invalidCredentials", result.message);
|
|
22
|
+
if (result.status >= 500) panic(`Expected magic-link sign-in to succeed, but got ${result.status}: ` + result.message);
|
|
23
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app signs people in with a magic link.`);
|
|
24
|
+
}
|
|
25
|
+
if (result.data.status === "MFA_REQUIRED") {
|
|
26
|
+
const { mfa } = result.data;
|
|
27
|
+
assert(mfa, "Expected a challenge from magic-link sign-in, but got none");
|
|
28
|
+
writeCookies([makePendingAuthCookie(mfa.challengeToken)]);
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
data: {
|
|
32
|
+
status: "authenticatorRequired",
|
|
33
|
+
step: mfa.action === "VERIFICATION_REQUIRED" ? "verify" : mfa.action === "SETUP_REQUIRED" ? "setup" : "setupOptional"
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const { session } = result.data;
|
|
38
|
+
assert(session, "Expected a session from magic-link sign-in, but got none");
|
|
39
|
+
writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
|
|
40
|
+
user: session.user,
|
|
41
|
+
permissions: session.permissions
|
|
42
|
+
}, session.jwt)]);
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
data: {
|
|
46
|
+
status: "signedIn",
|
|
47
|
+
user: session.user
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { signInWithMagicLink };
|
|
@@ -5,17 +5,31 @@ import { AuthError, Result } from "./utils.js";
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Signs a visitor in with an email and password.
|
|
8
|
+
*
|
|
9
|
+
* A `signedIn` result means the visitor is signed in. An app that also asks for
|
|
10
|
+
* an authenticator app answers `authenticatorRequired` instead, naming the step
|
|
11
|
+
* to send them to next.
|
|
8
12
|
*/
|
|
9
13
|
declare function signIn(credentials: {
|
|
10
14
|
email: string;
|
|
11
15
|
password: string;
|
|
12
|
-
}): Promise<Result<
|
|
16
|
+
}): Promise<Result<{
|
|
17
|
+
status: "signedIn";
|
|
18
|
+
user: User;
|
|
19
|
+
} | {
|
|
20
|
+
status: "authenticatorRequired";
|
|
21
|
+
/**
|
|
22
|
+
* `verify`: they have an authenticator app and owe a code.
|
|
23
|
+
* `setup`: the app requires one and they have none yet.
|
|
24
|
+
* `setupOptional`: same, except they may skip it instead.
|
|
25
|
+
*/
|
|
26
|
+
step: "verify" | "setup" | "setupOptional";
|
|
27
|
+
}, AuthError>>;
|
|
13
28
|
/**
|
|
14
|
-
* Signs the visitor out
|
|
29
|
+
* Signs the visitor out.
|
|
15
30
|
*
|
|
16
|
-
* This
|
|
17
|
-
*
|
|
18
|
-
* before it. There is no per-device sign-out.
|
|
31
|
+
* This ends **every** session that user has in this app, on every device. There
|
|
32
|
+
* is no per-device sign-out.
|
|
19
33
|
*/
|
|
20
34
|
declare function signOut(): Promise<void>;
|
|
21
35
|
//#endregion
|
|
@@ -1,44 +1,65 @@
|
|
|
1
|
-
import { panic, softAssert } from "../../assertions.js";
|
|
1
|
+
import { assert, panic, softAssert } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
4
|
import { makePlatform } from "../platform.js";
|
|
5
|
-
import { clearSession, makeSessionDataCookie, makeSessionTokenCookie, readSessionToken } from "../session.js";
|
|
5
|
+
import { clearPendingAuth, clearSession, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie, readSessionToken } from "../session.js";
|
|
6
6
|
import { authError } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/methods/password.ts
|
|
9
9
|
/**
|
|
10
10
|
* Signs a visitor in with an email and password.
|
|
11
|
+
*
|
|
12
|
+
* A `signedIn` result means the visitor is signed in. An app that also asks for
|
|
13
|
+
* an authenticator app answers `authenticatorRequired` instead, naming the step
|
|
14
|
+
* to send them to next.
|
|
11
15
|
*/
|
|
12
16
|
async function signIn(credentials) {
|
|
13
17
|
const result = await makePlatform(sessionConfig().applicationId).tokens(credentials);
|
|
14
18
|
if (!result.ok) {
|
|
15
|
-
if (result.status === 401
|
|
16
|
-
if (result.status
|
|
19
|
+
if (result.status === 401) return authError("invalidCredentials", result.message);
|
|
20
|
+
if (result.status === 429) return authError("tooManyAttempts", result.message);
|
|
21
|
+
if (result.status >= 500) panic(`Expected sign-in to succeed, but got ${result.status}: ` + result.message);
|
|
17
22
|
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows sign-in.`);
|
|
18
23
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
if (result.data.status === "MFA_REQUIRED") {
|
|
25
|
+
const { mfa } = result.data;
|
|
26
|
+
assert(mfa, "Expected a challenge from sign-in, but got none");
|
|
27
|
+
writeCookies([makePendingAuthCookie(mfa.challengeToken)]);
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
data: {
|
|
31
|
+
status: "authenticatorRequired",
|
|
32
|
+
step: mfa.action === "VERIFICATION_REQUIRED" ? "verify" : mfa.action === "SETUP_REQUIRED" ? "setup" : "setupOptional"
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const { session } = result.data;
|
|
37
|
+
assert(session, "Expected a session from sign-in, but got none");
|
|
38
|
+
writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
|
|
39
|
+
user: session.user,
|
|
40
|
+
permissions: session.permissions
|
|
41
|
+
}, session.jwt)]);
|
|
42
|
+
clearPendingAuth();
|
|
24
43
|
return {
|
|
25
44
|
success: true,
|
|
26
|
-
data:
|
|
45
|
+
data: {
|
|
46
|
+
status: "signedIn",
|
|
47
|
+
user: session.user
|
|
48
|
+
}
|
|
27
49
|
};
|
|
28
50
|
}
|
|
29
51
|
/**
|
|
30
|
-
* Signs the visitor out
|
|
52
|
+
* Signs the visitor out.
|
|
31
53
|
*
|
|
32
|
-
* This
|
|
33
|
-
*
|
|
34
|
-
* before it. There is no per-device sign-out.
|
|
54
|
+
* This ends **every** session that user has in this app, on every device. There
|
|
55
|
+
* is no per-device sign-out.
|
|
35
56
|
*/
|
|
36
57
|
async function signOut() {
|
|
37
58
|
const token = readSessionToken();
|
|
38
59
|
clearSession();
|
|
39
60
|
if (!token) return;
|
|
40
61
|
const result = await makePlatform(sessionConfig().applicationId).signOut(token);
|
|
41
|
-
softAssert(result.ok || result.status < 500, () => `
|
|
62
|
+
softAssert(result.ok || result.status < 500, () => `Expected sign-out to succeed, but got ${result.status}. This browser is signed out, other devices may not be.`);
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
//#endregion
|
|
@@ -13,25 +13,33 @@ declare function signUp(details: {
|
|
|
13
13
|
name?: string;
|
|
14
14
|
}): Promise<Result<null, AuthError>>;
|
|
15
15
|
/**
|
|
16
|
-
* Finishes sign-up
|
|
16
|
+
* Finishes a sign-up with the emailed code, creating the account and signing
|
|
17
|
+
* the visitor in.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* `password` is what the account is created with. Leave it out only for an app
|
|
20
|
+
* that signs people in with an emailed code. An app expecting a password
|
|
21
|
+
* rejects a sign-up without one, and the other way round.
|
|
22
|
+
*
|
|
23
|
+
* A `signedIn` result means the visitor is signed in. An app that also asks for
|
|
24
|
+
* an authenticator app answers `authenticatorRequired` instead, naming the step
|
|
25
|
+
* to send them to next.
|
|
22
26
|
*/
|
|
23
27
|
declare function confirmSignUp(details: {
|
|
24
28
|
email: string;
|
|
25
|
-
/**
|
|
26
|
-
* What `users/new` creates the account with, and unused on a code-only app,
|
|
27
|
-
* which never reaches that request.
|
|
28
|
-
*
|
|
29
|
-
* TODO: temporary in one respect — it is spent a second time on the
|
|
30
|
-
* 400-after-create workaround below, which goes away with the platform fix.
|
|
31
|
-
*/
|
|
32
|
-
password: string;
|
|
33
29
|
code: string;
|
|
34
30
|
name?: string;
|
|
35
|
-
|
|
31
|
+
password?: string;
|
|
32
|
+
}): Promise<Result<{
|
|
33
|
+
status: "signedIn";
|
|
34
|
+
user: User;
|
|
35
|
+
} | {
|
|
36
|
+
status: "authenticatorRequired";
|
|
37
|
+
/**
|
|
38
|
+
* `verify`: they have an authenticator app and owe a code.
|
|
39
|
+
* `setup`: the app requires one and they have none yet.
|
|
40
|
+
* `setupOptional`: same, except they may skip it instead.
|
|
41
|
+
*/
|
|
42
|
+
step: "verify" | "setup" | "setupOptional";
|
|
43
|
+
}, AuthError>>;
|
|
36
44
|
//#endregion
|
|
37
45
|
export { confirmSignUp, signUp };
|