@softr/sdk 0.0.93 → 0.0.94

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,
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.93";
2
+ var version = "0.0.94";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.93",
3
+ "version": "0.0.94",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: core
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  # Softr SDK core
@@ -1,19 +1,19 @@
1
1
  ---
2
2
  name: ai
3
3
  description: >-
4
- Run an AI model from the app on Softr's own keys, billed to the workspace's
5
- AI credits, with nothing to connect: defineAiAction from @softr/sdk declares
6
- the action with the catalog model it runs on and its permissions;
7
- ai.generate from @softr/sdk/server runs it with a system text, a prompt, an
8
- optional JSON schema for an answer that is data, and optional files;
9
- ai.stream runs it for an answer the user watches being written. Load this
10
- for a summary, an extraction, a classification, a draft, or a chat the app
11
- asks a model for, before you decide whether a feature runs on Softr AI
12
- or on the user's own account, and before you change the model it runs on.
4
+ Run an AI model from the app on Softr's own keys, billed to the workspace's AI
5
+ credits, with nothing to connect: defineAiAction from @softr/sdk declares the
6
+ action with the catalog model it runs on and its permissions; ai.generate from
7
+ @softr/sdk/server runs it with a system text, a prompt, an optional JSON
8
+ schema for an answer that is data, and optional files; ai.stream runs it for
9
+ an answer the user watches being written. Load this for a summary, an
10
+ extraction, a classification, a draft, or a chat the app asks a model for,
11
+ before you decide whether a feature runs on Softr AI or on the user's own
12
+ account, and before you change the model it runs on.
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.92
16
+ library_version: 0.0.94
17
17
  ---
18
18
 
19
19
  **Boundary.** `ai` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineAiAction` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -3,13 +3,13 @@ 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,
7
- updateProfile, and getAuthSettings and useAuthSettings for the methods the app
8
- enables. Covers the Result shape, its error codes, and toReturnPath.
6
+ confirmation, the emailed-code and authenticator-app steps, password reset
7
+ links, updateProfile, and getAuthSettings and useAuthSettings for the methods
8
+ the app enables. Covers the Result shape, its error codes, and toReturnPath.
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -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:
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  # The current user
@@ -7,7 +7,7 @@ description: >-
7
7
  metadata:
8
8
  type: sub-skill
9
9
  library: softr-sdk
10
- library_version: 0.0.92
10
+ library_version: 0.0.94
11
11
  ---
12
12
 
13
13
  **Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.92
11
+ library_version: 0.0.94
12
12
  ---
13
13
 
14
14
  # Dates, times and numbers
@@ -7,7 +7,7 @@ description: >-
7
7
  metadata:
8
8
  type: sub-skill
9
9
  library: softr-sdk
10
- library_version: 0.0.92
10
+ library_version: 0.0.94
11
11
  ---
12
12
 
13
13
  **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  # Installing the app
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.92
11
+ library_version: 0.0.94
12
12
  ---
13
13
 
14
14
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
@@ -3,13 +3,13 @@ name: routes-and-visibility
3
3
  description: >-
4
4
  Add a route file and declare its settings: definePage with its visibility
5
5
  (LOGGED_IN_USERS, NON_LOGGED_IN_USERS and customGroups) and its title,
6
- guardRoute or assertRouteAccess in beforeLoad, pageHead in head,
7
- isAuthorized and checkAccess anywhere, rendering an access error, and the
8
- lint rules that require the declaration.
6
+ guardRoute or assertRouteAccess in beforeLoad, pageHead in head, isAuthorized
7
+ and checkAccess anywhere, rendering an access error, and the lint rules that
8
+ require the declaration.
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `definePage`, `pageHead`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.92
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.