@softr/sdk 0.0.91 → 0.0.92-emailotc.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.
@@ -0,0 +1,34 @@
1
+ import { User } from "../user.js";
2
+ import { AuthError, Result } from "./utils.js";
3
+
4
+ //#region src/auth/methods/emailCode2fa.d.ts
5
+
6
+ /**
7
+ * Finishes a sign-in that answered `emailCodeRequired`, with the code emailed
8
+ * to the visitor.
9
+ *
10
+ * A `signedIn` result means the visitor is signed in. An app that also asks for
11
+ * an authenticator app answers `authenticatorRequired` instead, naming the step
12
+ * to send them to next.
13
+ */
14
+ declare function verifyEmailCode(details: {
15
+ code: string;
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>>;
28
+ /**
29
+ * Emails the code for {@link verifyEmailCode} again. Asking again within a
30
+ * short wait sends nothing and still succeeds.
31
+ */
32
+ declare function resendEmailCode(): Promise<Result<null, AuthError>>;
33
+ //#endregion
34
+ export { resendEmailCode, verifyEmailCode };
@@ -0,0 +1,71 @@
1
+ import { assert, panic } from "../../assertions.js";
2
+ import { writeCookies } from "../../cookies.js";
3
+ import { sessionConfig } from "../config.js";
4
+ import { clearPendingAuth, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken } from "../session.js";
5
+ import { makePlatform } from "../platform.js";
6
+ import { authError } from "./utils.js";
7
+
8
+ //#region src/auth/methods/emailCode2fa.ts
9
+ /**
10
+ * Finishes a sign-in that answered `emailCodeRequired`, with the code emailed
11
+ * to the visitor.
12
+ *
13
+ * A `signedIn` result means the visitor is signed in. An app that also asks for
14
+ * an authenticator app answers `authenticatorRequired` instead, naming the step
15
+ * to send them to next.
16
+ */
17
+ async function verifyEmailCode(details) {
18
+ const result = await makePlatform(sessionConfig().applicationId).emailCodeVerify(readPendingAuthToken(), details.code);
19
+ if (!result.ok) {
20
+ if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
21
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
22
+ if (result.status >= 500) panic(`Expected to verify the emailed code, but got ${result.status}: ` + result.message);
23
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app asks for an emailed code at sign-in.`);
24
+ }
25
+ if (result.data.status === "MFA_REQUIRED") {
26
+ const { mfa } = result.data;
27
+ assert(mfa, "Expected a challenge from verification, 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 verification, but got none");
39
+ writeCookies([...makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
40
+ user: session.user,
41
+ permissions: session.permissions
42
+ }, session.jwt)]);
43
+ clearPendingAuth();
44
+ return {
45
+ success: true,
46
+ data: {
47
+ status: "signedIn",
48
+ user: session.user
49
+ }
50
+ };
51
+ }
52
+ /**
53
+ * Emails the code for {@link verifyEmailCode} again. Asking again within a
54
+ * short wait sends nothing and still succeeds.
55
+ */
56
+ async function resendEmailCode() {
57
+ const result = await makePlatform(sessionConfig().applicationId).emailCodeResend(readPendingAuthToken());
58
+ if (!result.ok) {
59
+ if (result.status === 401) return authError("notAuthenticated", result.message);
60
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
61
+ if (result.status >= 500) panic(`Expected the code to be emailed again, but got ${result.status}: ` + result.message);
62
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app asks for an emailed code at sign-in.`);
63
+ }
64
+ return {
65
+ success: true,
66
+ data: null
67
+ };
68
+ }
69
+
70
+ //#endregion
71
+ export { resendEmailCode, verifyEmailCode };
@@ -8,7 +8,8 @@ import { AuthError, Result } from "./utils.js";
8
8
  *
9
9
  * A `signedIn` result means the visitor is signed in. An app that also asks for
10
10
  * an authenticator app answers `authenticatorRequired` instead, naming the step
11
- * to send them to next.
11
+ * to send them to next. An app that also asks for an emailed code answers
12
+ * `emailCodeRequired`; `verifyEmailCode` finishes it.
12
13
  */
13
14
  declare function signIn(credentials: {
14
15
  email: string;
@@ -24,6 +25,8 @@ declare function signIn(credentials: {
24
25
  * `setupOptional`: same, except they may skip it instead.
25
26
  */
26
27
  step: "verify" | "setup" | "setupOptional";
28
+ } | {
29
+ status: "emailCodeRequired";
27
30
  }, AuthError>>;
28
31
  /**
29
32
  * Signs the visitor out.
@@ -11,7 +11,8 @@ import { authError } from "./utils.js";
11
11
  *
12
12
  * A `signedIn` result means the visitor is signed in. An app that also asks for
13
13
  * an authenticator app answers `authenticatorRequired` instead, naming the step
14
- * to send them to next.
14
+ * to send them to next. An app that also asks for an emailed code answers
15
+ * `emailCodeRequired`; `verifyEmailCode` finishes it.
15
16
  */
16
17
  async function signIn(credentials) {
17
18
  const result = await makePlatform(sessionConfig().applicationId).tokens(credentials);
@@ -25,6 +26,10 @@ async function signIn(credentials) {
25
26
  const { mfa } = result.data;
26
27
  assert(mfa, "Expected a challenge from sign-in, but got none");
27
28
  writeCookies([makePendingAuthCookie(mfa.challengeToken)]);
29
+ if (mfa.action === "EMAIL_CODE_REQUIRED") return {
30
+ success: true,
31
+ data: { status: "emailCodeRequired" }
32
+ };
28
33
  return {
29
34
  success: true,
30
35
  data: {
@@ -71,6 +71,27 @@ function makePlatform(applicationId) {
71
71
  tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens${withAttributes}`, {
72
72
  method: "POST",
73
73
  json: credentials
74
+ }, z.object({
75
+ status: z.enum(["AUTHENTICATED", "MFA_REQUIRED"]),
76
+ session: z.object({
77
+ jwt: z.string(),
78
+ user: userSchema,
79
+ permissions: permissionsSchema
80
+ }).nullish(),
81
+ mfa: z.object({
82
+ action: z.enum([
83
+ "VERIFICATION_REQUIRED",
84
+ "SETUP_REQUIRED",
85
+ "SETUP_OPTIONAL",
86
+ "EMAIL_CODE_REQUIRED"
87
+ ]),
88
+ challengeToken: z.string()
89
+ }).nullish()
90
+ })),
91
+ emailCodeVerify: (token, code) => request(`/v1/vibe/applications/${applicationId}/users/tokens/email-code/verify${withAttributes}`, {
92
+ method: "POST",
93
+ token,
94
+ json: { code }
74
95
  }, z.object({
75
96
  status: z.enum(["AUTHENTICATED", "MFA_REQUIRED"]),
76
97
  session: z.object({
@@ -87,6 +108,10 @@ function makePlatform(applicationId) {
87
108
  challengeToken: z.string()
88
109
  }).nullish()
89
110
  })),
111
+ emailCodeResend: (token) => request(`/v1/vibe/applications/${applicationId}/users/tokens/email-code/resend`, {
112
+ method: "POST",
113
+ token
114
+ }, z.null()),
90
115
  sendSignInCode: (email) => request(`/v1/vibe/applications/${applicationId}/users/tokens/send-code`, {
91
116
  method: "POST",
92
117
  json: { email }
@@ -1,5 +1,6 @@
1
1
  import { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator } from "./methods/authenticator.js";
2
2
  import { sendSignInCode, signInWithCode } from "./methods/code.js";
3
+ import { resendEmailCode, verifyEmailCode } from "./methods/emailCode2fa.js";
3
4
  import { signInWithMagicLink } from "./methods/magicLink.js";
4
5
  import { signIn, signOut } from "./methods/password.js";
5
6
  import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./methods/passwordLink.js";
@@ -34,6 +35,8 @@ declare const auth: {
34
35
  sendAuthenticatorResetCode: typeof sendAuthenticatorResetCode;
35
36
  resetAuthenticator: typeof resetAuthenticator;
36
37
  removeAuthenticator: typeof removeAuthenticator;
38
+ verifyEmailCode: typeof verifyEmailCode;
39
+ resendEmailCode: typeof resendEmailCode;
37
40
  settings: typeof settings;
38
41
  updateProfile: typeof updateProfile;
39
42
  removeAvatar: typeof removeAvatar;
@@ -1,6 +1,7 @@
1
1
  import { settings } from "./methods/settings.js";
2
2
  import { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator } from "./methods/authenticator.js";
3
3
  import { sendSignInCode, signInWithCode } from "./methods/code.js";
4
+ import { resendEmailCode, verifyEmailCode } from "./methods/emailCode2fa.js";
4
5
  import { signInWithMagicLink } from "./methods/magicLink.js";
5
6
  import { signIn, signOut } from "./methods/password.js";
6
7
  import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./methods/passwordLink.js";
@@ -33,6 +34,8 @@ const auth = {
33
34
  sendAuthenticatorResetCode,
34
35
  resetAuthenticator,
35
36
  removeAuthenticator,
37
+ verifyEmailCode,
38
+ resendEmailCode,
36
39
  settings,
37
40
  updateProfile,
38
41
  removeAvatar,
@@ -1,4 +1,4 @@
1
- import * as react6 from "react";
1
+ import * as react2 from "react";
2
2
 
3
3
  //#region src/default-error-overlay.d.ts
4
4
  type Props = {
@@ -8,6 +8,6 @@ type Props = {
8
8
  declare function DefaultErrorOverlay({
9
9
  error,
10
10
  reset
11
- }: Props): react6.JSX.Element;
11
+ }: Props): react2.JSX.Element;
12
12
  //#endregion
13
13
  export { DefaultErrorOverlay };
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import * as react0 from "react";
2
+ import * as react3 from "react";
3
3
  import { ReactNode } from "react";
4
4
 
5
5
  //#region src/formatting.d.ts
@@ -94,7 +94,7 @@ declare function FormattingScope({
94
94
  /** A BCP 47 locale, such as `de-DE`. It drops the app's fixed date format inside the scope. */
95
95
  locale?: string;
96
96
  children?: ReactNode;
97
- }): react0.FunctionComponentElement<react0.ProviderProps<Scope>>;
97
+ }): react3.FunctionComponentElement<react3.ProviderProps<Scope>>;
98
98
  /**
99
99
  * A date in the app's format, safe to render on the server. A timestamp whose
100
100
  * zone is not known yet renders `fallback` until the browser supplies it, so
@@ -115,6 +115,6 @@ declare function FormattedDate({
115
115
  time?: boolean;
116
116
  /** What renders while the visitor's zone is unknown. */
117
117
  fallback?: ReactNode;
118
- }): react0.FunctionComponentElement<react0.FragmentProps>;
118
+ }): react3.FunctionComponentElement<react3.FragmentProps>;
119
119
  //#endregion
120
120
  export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.91";
2
+ var version = "0.0.92-emailotc.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/pwa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as react4 from "react";
1
+ import * as react0 from "react";
2
2
  import "@tanstack/react-start";
3
3
 
4
4
  //#region src/pwa.d.ts
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
23
23
  * )
24
24
  * }
25
25
  */
26
- declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
26
+ declare function SoftrPwaHead(): react0.FunctionComponentElement<react0.FragmentProps>;
27
27
  type PwaPlatform = "ios" | "android" | "desktop";
28
28
  type PwaInstall = {
29
29
  available: false;
package/package.json CHANGED
@@ -1,16 +1,37 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.91",
3
+ "version": "0.0.92-emailotc.0",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
7
- "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
8
- "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
9
- "./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
10
- },
11
- "bin": { "softr-sdk": "./dist/cli.js" },
12
- "files": ["dist", "schema", "skills", "!skills/_artifacts"],
13
- "publishConfig": { "access": "public" },
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ },
10
+ "./client": {
11
+ "types": "./dist/client.d.ts",
12
+ "import": "./dist/client.js"
13
+ },
14
+ "./server": {
15
+ "types": "./dist/server.d.ts",
16
+ "import": "./dist/server.js"
17
+ },
18
+ "./vite": {
19
+ "types": "./dist/vite.d.ts",
20
+ "import": "./dist/vite.js"
21
+ }
22
+ },
23
+ "bin": {
24
+ "softr-sdk": "./dist/cli.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "schema",
29
+ "skills",
30
+ "!skills/_artifacts"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
14
35
  "scripts": {
15
36
  "build": "tsdown",
16
37
  "test": "node --import ./src/test-utils/setup.ts --test",
@@ -42,7 +63,9 @@
42
63
  "optional": true
43
64
  }
44
65
  },
45
- "keywords": ["tanstack-intent"],
66
+ "keywords": [
67
+ "tanstack-intent"
68
+ ],
46
69
  "intent": {
47
70
  "version": 1,
48
71
  "repo": "@softr/sdk",
@@ -3,7 +3,7 @@ name: auth
3
3
  description: >-
4
4
  Sign a visitor in, out or up from a server function with auth from
5
5
  @softr/sdk/server: password, code, Google and magic link, sign-up and its
6
- confirmation, the authenticator-app steps, password reset links,
6
+ confirmation, the emailed-code and authenticator-app steps, password reset links,
7
7
  updateProfile, and getAuthSettings and useAuthSettings for the methods the app
8
8
  enables. Covers the Result shape, its error codes, and toReturnPath.
9
9
  metadata:
@@ -55,6 +55,13 @@ Signing out:
55
55
  | --- | --- |
56
56
  | `auth.signOut()` | Ends every session that visitor has in this app, on every device |
57
57
 
58
+ Finishing a sign-in that wants a code from the visitor's inbox:
59
+
60
+ | Call | Does |
61
+ | --- | --- |
62
+ | `auth.verifyEmailCode({ code })` | Answers it with the code that was emailed |
63
+ | `auth.resendEmailCode()` | Emails that code again |
64
+
58
65
  Finishing a sign-in that wants an authenticator app:
59
66
 
60
67
  | Call | Does |
@@ -197,6 +204,8 @@ Those calls take only what the visitor typed. The sign-in they finish is the one
197
204
 
198
205
  An app with no authenticator app configured never answers `authenticatorRequired`. The type still has both arms either way, so `result.data.user` does not exist until you have narrowed on `status`.
199
206
 
207
+ `signIn` has a third answer, `{ status: 'emailCodeRequired' }`, for an app that asks a password sign-in for a code from the visitor's inbox as well. The code is already on its way. Show a code field as the next step and finish with `auth.verifyEmailCode({ code })`, see [Finishing a sign-in that needs an emailed code](#finishing-a-sign-in-that-needs-an-emailed-code). That call answers like `signIn` does, so an app that also wants an authenticator app goes on to that step after it.
208
+
200
209
  ## Landing after sign-in
201
210
 
202
211
  A guard that turns a signed-out visitor away sends them to the page `softr.json` names for that, usually the login page. The page they wanted rides along in the search param named for the login page. `toReturnPath` from `@softr/sdk` reads it back:
@@ -255,10 +264,13 @@ if (answer.message !== null) return setMessage(answer.message)
255
264
  if (answer.outcome.status === 'authenticatorRequired') {
256
265
  return setStep({ name: 'secondFactor', opensOn: answer.outcome.step })
257
266
  }
267
+ if (answer.outcome.status === 'emailCodeRequired') {
268
+ return setStep({ name: 'emailCode' })
269
+ }
258
270
  await navigate({ href: target })
259
271
  ```
260
272
 
261
- Three exits: a message for the form to render, the second-factor step, the return target.
273
+ Four exits: a message for the form to render, the emailed-code step, the authenticator step, the return target.
262
274
 
263
275
  A server function that throws `redirect` needs the hook. Call the imported function directly and the throw never reaches the router, so a successful sign-in leaves the visitor on the form.
264
276
 
@@ -385,6 +397,22 @@ The UI has to carry email into step two, plus the name if step one collected one
385
397
 
386
398
  `password` is what the account is created with. Leave it out only for an app that signs people in with an emailed code. An app that expects a password rejects a sign-up without one, and an app that does not expect one rejects a sign-up that sends it.
387
399
 
400
+ ### Finishing a sign-in that needs an emailed code
401
+
402
+ After `signIn` answers `emailCodeRequired`, the visitor owes the code that was just emailed to them:
403
+
404
+ ```ts
405
+ // invalidCode for a wrong or expired code, otherwise the same two-armed result as signIn.
406
+ const result = await auth.verifyEmailCode({ code })
407
+
408
+ // For a "Resend code" link.
409
+ const sent = await auth.resendEmailCode()
410
+ ```
411
+
412
+ Send `authenticatorRequired` on to the authenticator step, never into the app.
413
+
414
+ Asking for a resend too soon sends nothing and still succeeds. It answers `notAuthenticated` once the sign-in has run out of time, and the visitor then signs in again from the start.
415
+
388
416
  ### Finishing a sign-in that needs an authenticator app
389
417
 
390
418
  On `step: 'verify'` the visitor already has an app, and owes a code from it: