@softr/sdk 0.0.93-alpha.2 → 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.
Files changed (36) hide show
  1. package/dist/action.d.ts +1 -46
  2. package/dist/action.js +1 -38
  3. package/dist/auth/methods/emailCode2fa.d.ts +34 -0
  4. package/dist/auth/methods/emailCode2fa.js +71 -0
  5. package/dist/auth/methods/password.d.ts +4 -1
  6. package/dist/auth/methods/password.js +6 -1
  7. package/dist/auth/platform.js +25 -0
  8. package/dist/auth/server.d.ts +3 -0
  9. package/dist/auth/server.js +3 -0
  10. package/dist/index.d.ts +2 -3
  11. package/dist/index.js +2 -2
  12. package/dist/package.js +1 -1
  13. package/dist/server.d.ts +1 -3
  14. package/dist/server.js +1 -3
  15. package/dist/vite.js +1 -20
  16. package/package.json +1 -1
  17. package/skills/softr-sdk/SKILL.md +4 -10
  18. package/skills/softr-sdk/ai/SKILL.md +1 -1
  19. package/skills/softr-sdk/auth/SKILL.md +33 -5
  20. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  21. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  22. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  23. package/skills/softr-sdk/formatting/SKILL.md +1 -1
  24. package/skills/softr-sdk/integrations/SKILL.md +1 -3
  25. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  26. package/skills/softr-sdk/pwa/SKILL.md +1 -1
  27. package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
  28. package/skills/softr-sdk/record-queries/SKILL.md +1 -1
  29. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  30. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  31. package/skills/softr-sdk/workflows/SKILL.md +8 -10
  32. package/dist/core/email.d.ts +0 -52
  33. package/dist/core/email.js +0 -62
  34. package/dist/core/step.d.ts +0 -20
  35. package/dist/core/step.js +0 -48
  36. package/skills/softr-sdk/email/SKILL.md +0 -127
package/dist/action.d.ts CHANGED
@@ -234,51 +234,6 @@ type IntegrationDefinition<TId extends string> = {
234
234
  * const res = await listEvents({ query: { timeMin: now, singleEvents: "true" } })
235
235
  */
236
236
  declare function defineIntegration<const TId extends string>(definition: IntegrationDefinition<TId>): IntegrationAction<TId>;
237
- type EmailInputs = {
238
- senderId?: string;
239
- to?: ReadonlyArray<string>;
240
- cc?: ReadonlyArray<string>;
241
- bcc?: ReadonlyArray<string>;
242
- replyTo?: string;
243
- };
244
- type EmailResource = {
245
- type: "WORKFLOW_STEP";
246
- stepType: "SOFTR_SEND_EMAIL";
247
- inputs: EmailInputs;
248
- };
249
- type EmailAction<TId extends string = string> = Action<EmailResource> & {
250
- id: TId;
251
- };
252
- type EmailDefinition<TId extends string> = EmailInputs & {
253
- id: TId;
254
- permissions: ActionPermissions;
255
- };
256
- /**
257
- * Declares an email action: who may send it, and the fields every email it sends keeps whatever the
258
- * call says. Softr sends it from the app's email sender and bills the workspace's workflow credits.
259
- *
260
- * - `to`, `cc`, `bcc`: addresses. An element may be a whole `{LOGGED_IN_USER:EMAIL}` or
261
- * `{LOGGED_IN_USER:<field>}` placeholder, filled in for the signed-in sender.
262
- * - `replyTo`: an address or one such placeholder.
263
- * - `senderId`: one of the workspace's email senders, for an email that must come from that address.
264
- * Left out, the app's sender is used. The call can never choose the sender.
265
- *
266
- * A field declared here wins over the call's, and declaring `to` also ignores the call's `cc` and
267
- * `bcc`. Fix the recipients whenever the use case allows,
268
- * because anyone in the audience can otherwise send any email to anyone.
269
- *
270
- * Pass the returned object to `email.send()` at runtime.
271
- *
272
- * @example
273
- * const notifyOwner = defineEmailAction({
274
- * id: "notifyOwner",
275
- * to: ["owner@acme.com"],
276
- * permissions: { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] },
277
- * })
278
- *
279
- * await email.send(notifyOwner, { subject: "New submission", body: html, replyTo: data.email })
280
- */
281
- declare function defineEmailAction<const TId extends string>(definition: EmailDefinition<TId>): EmailAction<TId>;
282
237
  type AiResource = {
283
238
  type: "SOFTR_AI";
284
239
  model: string;
@@ -313,4 +268,4 @@ type AiDefinition<TId extends string> = {
313
268
  */
314
269
  declare function defineAiAction<const TId extends string>(definition: AiDefinition<TId>): AiAction<TId>;
315
270
  //#endregion
316
- export { ActionData, ActionPermissions, ActionRecord, ActionShape, AiAction, DatasourceAction, EmailAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
271
+ export { ActionData, ActionPermissions, ActionRecord, ActionShape, AiAction, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
package/dist/action.js CHANGED
@@ -50,43 +50,6 @@ function defineIntegration(definition) {
50
50
  permissions
51
51
  };
52
52
  }
53
- /**
54
- * Declares an email action: who may send it, and the fields every email it sends keeps whatever the
55
- * call says. Softr sends it from the app's email sender and bills the workspace's workflow credits.
56
- *
57
- * - `to`, `cc`, `bcc`: addresses. An element may be a whole `{LOGGED_IN_USER:EMAIL}` or
58
- * `{LOGGED_IN_USER:<field>}` placeholder, filled in for the signed-in sender.
59
- * - `replyTo`: an address or one such placeholder.
60
- * - `senderId`: one of the workspace's email senders, for an email that must come from that address.
61
- * Left out, the app's sender is used. The call can never choose the sender.
62
- *
63
- * A field declared here wins over the call's, and declaring `to` also ignores the call's `cc` and
64
- * `bcc`. Fix the recipients whenever the use case allows,
65
- * because anyone in the audience can otherwise send any email to anyone.
66
- *
67
- * Pass the returned object to `email.send()` at runtime.
68
- *
69
- * @example
70
- * const notifyOwner = defineEmailAction({
71
- * id: "notifyOwner",
72
- * to: ["owner@acme.com"],
73
- * permissions: { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] },
74
- * })
75
- *
76
- * await email.send(notifyOwner, { subject: "New submission", body: html, replyTo: data.email })
77
- */
78
- function defineEmailAction(definition) {
79
- const { id, permissions,...inputs } = definition;
80
- return {
81
- id,
82
- resource: {
83
- type: "WORKFLOW_STEP",
84
- stepType: "SOFTR_SEND_EMAIL",
85
- inputs
86
- },
87
- permissions
88
- };
89
- }
90
53
  function isDatasourceAction(value) {
91
54
  if (typeof value !== "object" || value === null) return false;
92
55
  const candidate = value;
@@ -106,4 +69,4 @@ function defineAiAction(definition) {
106
69
  }
107
70
 
108
71
  //#endregion
109
- export { defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
72
+ export { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
@@ -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/index.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, User } from "./auth/user.js";
2
- import { ActionPermissions, SoftrDatasources, defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
+ import { ActionPermissions, SoftrDatasources, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
3
3
  import { FileAttachment } from "./core/file-upload.js";
4
4
  import { FileItem, useFileUpload } from "./core/use-file-upload.js";
5
5
  import { AiAnswer, AiFile, AiGenerateInit, AiMessage, AiUsage } from "./core/ai.js";
6
- import { EmailAttachment, EmailSendInit, EmailSent } from "./core/email.js";
7
6
  import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
8
7
  import { SoftrContext } from "./auth/context.js";
9
8
  import { AuthSettings } from "./auth/platform.js";
@@ -19,4 +18,4 @@ import { useUserTimezone } from "./timezone.js";
19
18
  import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
20
19
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
21
20
  import { Audience } from "./route/audience.js";
22
- export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type EmailAttachment, type EmailSendInit, type EmailSent, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineEmailAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
21
+ export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
1
+ import { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
2
  import { useFileUpload } from "./core/use-file-upload.js";
3
3
  import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
4
4
  import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
@@ -14,4 +14,4 @@ import { useUserTimezone } from "./timezone.js";
14
14
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
15
15
  import { Audience } from "./route/audience.js";
16
16
 
17
- export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineAiAction, defineEmailAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
17
+ export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineAiAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.93-alpha.2";
2
+ var version = "0.0.94";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/server.d.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import { upload } from "./core/file-upload.js";
2
2
  import { AiError, ai } from "./core/ai.js";
3
- import { StepError, StepErrorCode } from "./core/step.js";
4
- import { EmailError, EmailErrorCode, email } from "./core/email.js";
5
3
  import { auth } from "./auth/server.js";
6
4
  import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
7
5
  import { ds } from "./datasource/datasource.js";
8
6
  import { workflow } from "./core/workflow.js";
9
7
  import { integration } from "./core/integration.js";
10
8
  import "@tanstack/react-start/server-only";
11
- export { AiError, EmailError, type EmailErrorCode, type FieldChoice, type RecordOption, type RecordOptions, StepError, type StepErrorCode, ai, auth, ds, email, integration, upload, workflow };
9
+ export { AiError, type FieldChoice, type RecordOption, type RecordOptions, ai, auth, ds, integration, upload, workflow };
package/dist/server.js CHANGED
@@ -4,8 +4,6 @@ import { auth } from "./auth/server.js";
4
4
  import { workflow } from "./core/workflow.js";
5
5
  import { integration } from "./core/integration.js";
6
6
  import { AiError, ai } from "./core/ai.js";
7
- import { StepError } from "./core/step.js";
8
- import { EmailError, email } from "./core/email.js";
9
7
  import "@tanstack/react-start/server-only";
10
8
 
11
- export { AiError, EmailError, StepError, ai, auth, ds, email, integration, upload, workflow };
9
+ export { AiError, ai, auth, ds, integration, upload, workflow };
package/dist/vite.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import { SOFTR_CONFIG_FILE, loadSoftrConfig } from "./softr-config.schema.js";
2
2
  import path from "node:path";
3
- import { pathToFileURL } from "node:url";
4
3
 
5
4
  //#region src/vite.ts
6
5
  function softr() {
7
- return [softrConfig(), editEngine()];
6
+ return [softrConfig()];
8
7
  }
9
8
  function softrConfig() {
10
9
  let root;
@@ -23,24 +22,6 @@ function softrConfig() {
23
22
  }
24
23
  };
25
24
  }
26
- /**
27
- * The edit engine is provided by the pod runtime rather than an app dependency: it is never in the
28
- * app's `package.json` and never in its `node_modules`. The pod points `SOFTR_EDIT_ENGINE_DIR` at
29
- * the copy baked into its image, and this function loads it from there.
30
- *
31
- * If the env var is unset, this plugin is a no-op. It's only loaded in the preview mode
32
- */
33
- async function editEngine() {
34
- const dir = process.env.SOFTR_EDIT_ENGINE_DIR;
35
- if (!dir) return false;
36
- const entry = pathToFileURL(path.join(dir, "dist", "vite.js")).href;
37
- try {
38
- return (await import(entry)).editEngineStudio();
39
- } catch (error) {
40
- console.error(`[softr] could not load the edit engine from ${entry}:`, error);
41
- return false;
42
- }
43
- }
44
25
 
45
26
  //#endregion
46
27
  export { softr };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.93-alpha.2",
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.93-alpha.2
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  # Softr SDK core
@@ -22,8 +22,6 @@ metadata:
22
22
  >
23
23
  > **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register `softrAuthMiddleware` and resolve the session. Do not add a second registration of it. `softrMcpMiddleware` is the one thing an app adds to `src/start.ts` itself — load the `softr-sdk/mcp` sub-skill before doing so.
24
24
  >
25
- > **CRITICAL**: `email.send` lives on `@softr/sdk/server` and only runs inside a `createServerFn` handler. Use `defineEmailAction` from `@softr/sdk` to declare who may send it and the recipients it fixes. An email the app sends itself needs no workflow and no connected mail service. Load the `softr-sdk/email` sub-skill for the full pattern.
26
- >
27
25
  > **CRITICAL**: `ai.generate` and `ai.stream` live on `@softr/sdk/server` and only run inside a `createServerFn` handler. Use `defineAiAction` from `@softr/sdk` to declare the action with the model it runs on, an id copied from the `ai_list_models` tool. Softr runs the model on its own keys; never ask the user for an API key for the app's own AI feature. Load the `softr-sdk/ai` sub-skill for the full pattern.
28
26
  >
29
27
  > **CRITICAL**: `workflow.trigger` lives on `@softr/sdk/server` and only runs inside a `createServerFn` handler. Use `defineWorkflow` from `@softr/sdk` to declare the action, then pass it to `workflow.trigger`. Load the `softr-sdk/workflows` sub-skill for the full pattern.
@@ -34,8 +32,8 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
34
32
 
35
33
  | Import | Runs | Holds |
36
34
  | --- | --- | --- |
37
- | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineEmailAction`, `defineAiAction`, `definePage`, `pageHead`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
38
- | `@softr/sdk/server` | Server only | `ds`, `auth`, `email`, `EmailError`, `workflow`, `integration`, `ai` |
35
+ | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineAiAction`, `definePage`, `pageHead`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
36
+ | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration`, `ai` |
39
37
  | `@softr/sdk/client` | Browser only | `useFileUpload` |
40
38
  | `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
41
39
 
@@ -49,11 +47,10 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
49
47
  | Fetch records from a datasource | [record-queries/SKILL.md](./record-queries/SKILL.md) |
50
48
  | Chart, aggregate or count records | [charts-and-metrics/SKILL.md](./charts-and-metrics/SKILL.md) |
51
49
  | Create, update or delete a record | [record-mutations/SKILL.md](./record-mutations/SKILL.md) |
52
- | Send an email from the app: a form that emails the owner, a confirmation, staff emailing a client | [email/SKILL.md](./email/SKILL.md) |
53
50
  | Trigger a Softr workflow | [workflows/SKILL.md](./workflows/SKILL.md) |
54
51
  | Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
55
52
  | Ask an AI model: a summary, an extraction from a document, a classification, a draft, a chat | [ai/SKILL.md](./ai/SKILL.md) |
56
- | Call a connected third-party API (Slack, Stripe, Gmail from the user's own mailbox, …), or an AI model on the user's own provider account | [integrations/SKILL.md](./integrations/SKILL.md) |
53
+ | Call a connected third-party API (Slack, Stripe, Gmail, …), or an AI model on the user's own provider account | [integrations/SKILL.md](./integrations/SKILL.md) |
57
54
  | Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
58
55
  | Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
59
56
  | Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
@@ -79,9 +76,6 @@ Drawing a chart, a stat tile, or any aggregated number?
79
76
  Submitting a form, editing a record, or deleting one?
80
77
  load softr-sdk/record-mutations
81
78
 
82
- Sending an email when a form is submitted or a button is pressed?
83
- load softr-sdk/email — no workflow, no connected mail service
84
-
85
79
  Triggering a Softr workflow from a button or form?
86
80
  load softr-sdk/workflows
87
81
 
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.93-alpha.2
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.93-alpha.2
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.93-alpha.2
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.93-alpha.2
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.93-alpha.2
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.93-alpha.2
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.93-alpha.2
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.
@@ -16,8 +16,6 @@ metadata:
16
16
 
17
17
  `integration.createFetcher` calls a third-party API through Softr's authenticated proxy. It adds the connected account's credentials automatically — you never set API keys or auth headers yourself. **Every call acts as that one account, whoever the visitor is**, so take what a call reaches, such as a customer or a channel, from the session or a record the server read, never from the arguments the browser sends.
18
18
 
19
- An email the app sends is the `email` sub-skill, with nothing to connect. Send through a connected Gmail or Outlook only when the user wants the email to come from their own mailbox.
20
-
21
19
  ## Setup
22
20
 
23
21
  ```ts
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.93-alpha.2
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.93-alpha.2
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.93-alpha.2
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.93-alpha.2
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.93-alpha.2
12
+ library_version: 0.0.94
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` 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.93-alpha.2
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.93-alpha.2
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.
@@ -18,8 +18,6 @@ metadata:
18
18
 
19
19
  `workflow.trigger` fires a Softr workflow and waits for it to finish. It resolves to `void`.
20
20
 
21
- An email the app sends itself, when a form is submitted or a button is pressed, needs no workflow: load the `email` sub-skill. Fire a workflow for what the workflow's own steps do, such as updating a CRM or posting to a connected service.
22
-
23
21
  ## Setup
24
22
 
25
23
  ```ts
@@ -33,15 +31,15 @@ import { workflow } from '@softr/sdk/server'
33
31
  Every trigger needs a workflow action: which workflow, which trigger step, who may run it, and any data that is always sent. The workflow must have a **Run custom workflow** trigger — no other trigger type can be fired from app code.
34
32
 
35
33
  ```ts
36
- const syncNewLead = defineWorkflow({
37
- id: 'syncNewLead',
34
+ const sendWelcomeEmail = defineWorkflow({
35
+ id: 'sendWelcomeEmail',
38
36
  workflowId: 'c9d8e7f6-a5b4-4c3d-8e2f-1a0b9c8d7e6f',
39
37
  triggerId: '926da496-749f-4006-999c-197d8a556122',
40
38
  permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
41
39
  body: {
42
40
  email: '{LOGGED_IN_USER:EMAIL}', // resolved server-side from the signed-in user
43
41
  name: '{LOGGED_IN_USER:NAME}',
44
- source: 'website', // any hardcoded value — always sent, never overridden at runtime
42
+ plan: 'enterprise', // any hardcoded value — always sent, never overridden at runtime
45
43
  },
46
44
  })
47
45
  ```
@@ -72,7 +70,7 @@ These placeholders are resolved server-side when the workflow runs:
72
70
  const runWorkflow = createServerFn({ method: 'POST' })
73
71
  .validator(z.object({ message: z.string() }))
74
72
  .handler(async ({ data }) => {
75
- await workflow.trigger(syncNewLead, { message: data.message })
73
+ await workflow.trigger(sendWelcomeEmail, { message: data.message })
76
74
  })
77
75
  ```
78
76
 
@@ -84,7 +82,7 @@ const runWorkflow = createServerFn({ method: 'POST' })
84
82
 
85
83
  ```ts
86
84
  const run = createServerFn({ method: 'POST' }).handler(async () => {
87
- await workflow.trigger(syncNewLead)
85
+ await workflow.trigger(sendWelcomeEmail)
88
86
  })
89
87
  ```
90
88
 
@@ -96,11 +94,11 @@ Omit the second argument when the action's `body` already carries everything the
96
94
 
97
95
  ```ts
98
96
  // Wrong: calling from a click handler
99
- <button onClick={() => workflow.trigger(syncNewLead)}>Run</button>
97
+ <button onClick={() => workflow.trigger(sendWelcomeEmail)}>Run</button>
100
98
 
101
99
  // Correct: server function, called from the button
102
100
  const run = createServerFn({ method: 'POST' }).handler(() =>
103
- workflow.trigger(syncNewLead),
101
+ workflow.trigger(sendWelcomeEmail),
104
102
  )
105
103
  // …
106
104
  const submit = useServerFn(run)
@@ -1,52 +0,0 @@
1
- import { EmailAction } from "../action.js";
2
- import { StepError, StepErrorCode } from "./step.js";
3
-
4
- //#region src/core/email.d.ts
5
- /** A file the email carries, by the URL the app holds it under. */
6
- interface EmailAttachment {
7
- name: string;
8
- /** The URL of a file uploaded to this app with `upload`. */
9
- url: string;
10
- }
11
- interface EmailSendInit {
12
- /** Ignored when the action declares its own. */
13
- to?: ReadonlyArray<string>;
14
- /** Ignored when the action declares its own `cc` or `to`. */
15
- cc?: ReadonlyArray<string>;
16
- /** Ignored when the action declares its own `bcc` or `to`. */
17
- bcc?: ReadonlyArray<string>;
18
- /** Ignored when the action declares its own. */
19
- replyTo?: string;
20
- subject: string;
21
- /** Plain text or HTML. */
22
- body: string;
23
- /** Files uploaded to this app with `upload`; any other URL is refused. */
24
- attachments?: ReadonlyArray<EmailAttachment>;
25
- /**
26
- * The same key sends the email once, however often the call is repeated. Pass one derived from
27
- * what the email is about, such as the id of the record it announces, when the call may be retried.
28
- * At most 128 characters. Left out, every call gets a fresh one.
29
- */
30
- idempotencyKey?: string;
31
- }
32
- interface EmailSent {
33
- /** Null when the mail provider did not report one; the email was still sent. */
34
- messageId: string | null;
35
- }
36
- type EmailErrorCode = StepErrorCode;
37
- /**
38
- * Thrown when Softr did not send the email; a {@link StepError}, so `code` says why. For an email, `INVALID_INPUT`
39
- * is a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment
40
- * that is not one of the app's uploads, or an idempotency key over 128 characters; `UNAVAILABLE` includes the Softr
41
- * fallback address past its daily limit and an unconfirmed sender.
42
- */
43
- declare class EmailError extends StepError {
44
- constructor(status: number, code: EmailErrorCode, message: string);
45
- }
46
- /** Sends one email through the action. Never retries by itself. */
47
- declare function sendEmail(action: EmailAction, init: EmailSendInit): Promise<EmailSent>;
48
- declare const email: {
49
- send: typeof sendEmail;
50
- };
51
- //#endregion
52
- export { EmailAttachment, EmailError, EmailErrorCode, EmailSendInit, EmailSent, email };
@@ -1,62 +0,0 @@
1
- import { assert } from "../assertions.js";
2
- import { softrApiUrl } from "../softr-api.js";
3
- import { fetch } from "../fetch.js";
4
- import { sessionConfig } from "../auth/config.js";
5
- import { readSessionToken } from "../auth/session.js";
6
- import { StepError, readStepFailure } from "./step.js";
7
-
8
- //#region src/core/email.ts
9
- /** Longer than Softr waits for the send itself, so its own timeout answers first. */
10
- const TIMEOUT_MS = 12e4;
11
- /**
12
- * Thrown when Softr did not send the email; a {@link StepError}, so `code` says why. For an email, `INVALID_INPUT`
13
- * is a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment
14
- * that is not one of the app's uploads, or an idempotency key over 128 characters; `UNAVAILABLE` includes the Softr
15
- * fallback address past its daily limit and an unconfirmed sender.
16
- */
17
- var EmailError = class extends StepError {
18
- constructor(status, code, message) {
19
- super(status, code, message);
20
- this.name = "EmailError";
21
- }
22
- };
23
- /** Sends one email through the action. Never retries by itself. */
24
- async function sendEmail(action, init) {
25
- const { to, cc, bcc, replyTo, subject, body, attachments } = init;
26
- const sessionToken = readSessionToken();
27
- let res;
28
- try {
29
- res = await fetch(`${softrApiUrl("SOFTR_PLATFORM_API_URL")}/v1/vibe/applications/${sessionConfig().applicationId}/actions/${action.id}/step`, {
30
- method: "POST",
31
- headers: {
32
- ...sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {},
33
- "Content-Type": "application/json",
34
- "Idempotency-Key": init.idempotencyKey ?? crypto.randomUUID()
35
- },
36
- body: JSON.stringify({ input: {
37
- to,
38
- cc,
39
- bcc,
40
- replyTo,
41
- subject,
42
- body,
43
- attachments
44
- } }),
45
- signal: AbortSignal.timeout(TIMEOUT_MS)
46
- });
47
- } catch (error) {
48
- if (error instanceof DOMException && error.name === "TimeoutError") throw new EmailError(504, "TIMEOUT", `Email "${action.id}" got no answer in time. It may still have been sent.`);
49
- throw new EmailError(502, "UNAVAILABLE", `Email "${action.id}" could not reach Softr: ${error instanceof Error ? error.message : String(error)}`);
50
- }
51
- if (res.ok) {
52
- const sent = await res.json();
53
- assert(typeof sent === "object" && sent !== null && "messageId" in sent && (typeof sent.messageId === "string" || sent.messageId === null), "the send answered without a messageId");
54
- return { messageId: sent.messageId };
55
- }
56
- const { code, message } = await readStepFailure(res, `Email "${action.id}" failed with HTTP ${res.status}`);
57
- throw new EmailError(res.status, code, message);
58
- }
59
- const email = { send: sendEmail };
60
-
61
- //#endregion
62
- export { EmailError, email };
@@ -1,20 +0,0 @@
1
- //#region src/core/step.d.ts
2
- /** Why Softr did not run a workflow step the app runs by itself, whatever the step. */
3
- declare const STEP_ERROR_CODES: readonly ["NOT_FOUND", "INVALID_INPUT", "UNAVAILABLE", "TIMEOUT", "RATE_LIMITED"];
4
- type StepErrorCode = (typeof STEP_ERROR_CODES)[number];
5
- /**
6
- * Thrown when Softr did not run a step the app runs by itself, such as sending an email. `code` says why:
7
- *
8
- * - `NOT_FOUND`: no such action, or the caller is not in its audience.
9
- * - `INVALID_INPUT`: the call's inputs were refused; the message says which.
10
- * - `UNAVAILABLE`: Softr could not run it, for example with no workflow credits left, or could not be reached.
11
- * - `TIMEOUT`: no answer in time. It may still have run; retry with the same idempotency key.
12
- * - `RATE_LIMITED`: the app ran its actions too often in the last minute or day; try again later.
13
- */
14
- declare class StepError extends Error {
15
- readonly status: number;
16
- readonly code: StepErrorCode;
17
- constructor(status: number, code: StepErrorCode, message: string);
18
- }
19
- //#endregion
20
- export { StepError, StepErrorCode };
package/dist/core/step.js DELETED
@@ -1,48 +0,0 @@
1
- //#region src/core/step.ts
2
- /** Why Softr did not run a workflow step the app runs by itself, whatever the step. */
3
- const STEP_ERROR_CODES = [
4
- "NOT_FOUND",
5
- "INVALID_INPUT",
6
- "UNAVAILABLE",
7
- "TIMEOUT",
8
- "RATE_LIMITED"
9
- ];
10
- /**
11
- * Thrown when Softr did not run a step the app runs by itself, such as sending an email. `code` says why:
12
- *
13
- * - `NOT_FOUND`: no such action, or the caller is not in its audience.
14
- * - `INVALID_INPUT`: the call's inputs were refused; the message says which.
15
- * - `UNAVAILABLE`: Softr could not run it, for example with no workflow credits left, or could not be reached.
16
- * - `TIMEOUT`: no answer in time. It may still have run; retry with the same idempotency key.
17
- * - `RATE_LIMITED`: the app ran its actions too often in the last minute or day; try again later.
18
- */
19
- var StepError = class extends Error {
20
- status;
21
- code;
22
- constructor(status, code, message) {
23
- super(message);
24
- this.name = "StepError";
25
- this.status = status;
26
- this.code = code;
27
- }
28
- };
29
- /** The code and message of a step Softr refused, read from its answer; `fallbackMessage` when it sent none. */
30
- async function readStepFailure(res, fallbackMessage) {
31
- const failure = await res.json().catch(() => null);
32
- const known = typeof failure === "object" && failure !== null ? failure : {};
33
- return {
34
- code: ("code" in known ? STEP_ERROR_CODES.find((errorCode) => errorCode === known.code) : void 0) ?? codeOfStatus(res.status),
35
- message: "message" in known && typeof known.message === "string" && known.message !== "" ? known.message : fallbackMessage
36
- };
37
- }
38
- function codeOfStatus(status) {
39
- switch (status) {
40
- case 400: return "INVALID_INPUT";
41
- case 404: return "NOT_FOUND";
42
- case 429: return "RATE_LIMITED";
43
- default: return "UNAVAILABLE";
44
- }
45
- }
46
-
47
- //#endregion
48
- export { StepError, readStepFailure };
@@ -1,127 +0,0 @@
1
- ---
2
- name: email
3
- description: >-
4
- Send an email from the app itself, with nothing to connect and no workflow:
5
- defineEmailAction from @softr/sdk declares who may send it and the fields it
6
- fixes (to, cc, bcc, replyTo, and an optional senderId); email.send from
7
- @softr/sdk/server sends one with a subject, a text or HTML body and optional
8
- attachments, and throws an EmailError with a code when Softr does not send it.
9
- Load this for a contact form that emails the owner, a confirmation to the
10
- signed-in user, staff emailing a client, or any email the app sends while
11
- handling what a person does in it.
12
- metadata:
13
- type: sub-skill
14
- library: softr-sdk
15
- library_version: 0.0.93-alpha.2
16
- ---
17
-
18
- **Boundary.** `email` and `EmailError` live on `@softr/sdk/server` and run only inside a `createServerFn` handler — importing them into a component fails the build. `defineEmailAction` and every type come from `@softr/sdk` and can be imported anywhere.
19
-
20
- # Email
21
-
22
- An app sends an email through an email action. Softr sends it from the app's email sender and bills the workspace's **workflow credits**, so there is no mail service to connect and no workflow to build. Use it for every email the app sends while it handles something a person does in it: a form submitted, a button pressed, a record saved through the app.
23
-
24
- A workflow is still the answer for an email on a schedule ("every Monday") or on a change made outside the app (a record edited in the table): load `automate-with-workflows` for those. A connected Gmail or Outlook is the answer only when the user wants the email to come from their own mailbox, through the `integrations` sub-skill.
25
-
26
- `defineEmailAction` missing from `@softr/sdk`? The app was built on an older version. Upgrade `@softr/sdk` to the latest version first, then write the action.
27
-
28
- ## Who can send it, and to whom
29
-
30
- The action's `permissions` is its audience: everyone in it can send an email through it, directly, without the app's own code. The app's code is not a guard. Only what the declaration fixes holds whatever the caller sends.
31
-
32
- - **Fix the recipients whenever the use case allows.** A contact form that emails the owner fixes `to` to the owner. "Email me this report" fixes `to` to `{LOGGED_IN_USER:EMAIL}`. Then nobody can send the app's email to anyone else.
33
- - **When people pick the recipients, narrow the audience.** "Staff email a client" leaves `to` open and keeps the action to the staff group with `customGroups`. An action with open recipients for all signed-in users lets anyone who signs up send any email to anyone from the app's address, up to the workspace's credits. Build that only when the user asks for it, and tell them so in one line. An action open to visitors must fix `to`, `cc` or `bcc`: one that fixes none is not stored.
34
-
35
- ## The sender
36
-
37
- Every email comes from the app's email sender, unless the action names one of the workspace's senders with `senderId`.
38
-
39
- - Read the app's sender with `application_get` before you say where the emails come from, and name it. Never write "if no sender is set": read it. That tool shows the sign-in emails' default, an `@softr.app` address, when no sender is set. Emails from an email action then come from Softr's address instead: the workspace's name at `<workspaceId>@softr-email.com`, limited to 30 emails a day for the whole workspace, its workflows included. That address is for testing only. Say so in your closing message when no sender is set, and that the app needs its own sender before real users rely on its emails.
40
- - When the app has no sender of its own, list the workspace's senders with `workspace_list_email_senders` and offer the confirmed ones by name: for this email (`senderId`, nothing else changes) or as the app's sender. Never pick one yourself.
41
- - Changing the app's sender also changes the address its sign-in emails come from. Ask the user before you call `application_configure_email_sender`, and name both effects. A workspace whose plan has no custom senders is refused; then say that sending from their own address needs the Professional plan.
42
- - Set `senderId` only when the user wants this email from a specific address, such as `billing@`. Pick it from `workspace_list_email_senders`, copied exactly. A sender the mail provider has not confirmed makes every send fail until it is confirmed. That confirmation is all an email action needs; the domain's DNS records (DKIM, Return-Path) are needed only before Studio lets the app pick the address as its own sender, and they help the emails stay out of spam.
43
-
44
- ## What it costs
45
-
46
- Every email costs **5 workflow credits per recipient**, from the workspace's monthly workflow credits, which its workflows spend too. You do not know the workspace's plan, so never name one or count how many emails it covers. The plan and the closing message give the cost and where to watch it, for example: "Each submission emails you, at 5 workflow credits per email. Track them in Settings → Billing → Workflow usage, where this app has its own row."
47
-
48
- Emails sent from the preview are real and charged, and the actions in the code reach the live app as soon as they are saved. Send at most one test email, and only when the user asks for one. Never send emails in a loop to try something out.
49
-
50
- ## Declare the action
51
-
52
- ```ts
53
- const notifyOwner = defineEmailAction({
54
- id: 'notifyOwner',
55
- to: ['owner@acme.com'],
56
- permissions: { userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'] },
57
- })
58
-
59
- const emailClient = defineEmailAction({
60
- id: 'emailClient',
61
- replyTo: '{LOGGED_IN_USER:EMAIL}',
62
- permissions: { userGroupTypes: ['LOGGED_IN_USERS'], customGroups: ['<the staff group id>'] },
63
- })
64
- ```
65
-
66
- - `id` is a name you choose, unique across the app.
67
- - `to`, `cc`, `bcc`: arrays of addresses. Each one declared here replaces the call's, and declaring `to` also ignores the call's `cc` and `bcc`. An element may be a whole placeholder for the signed-in sender: `{LOGGED_IN_USER:EMAIL}`, or `{LOGGED_IN_USER:<field>}` for an email field of their user record. Never part of a string, never in the call.
68
- - `replyTo`: an address or one such placeholder.
69
- - `senderId`: one of the workspace's senders, as above. The call can never set the sender.
70
- - `permissions`: who may send it, as for queries and mutations.
71
-
72
- Write the object inline in `defineEmailAction`, every value a literal. A declaration that cannot be read is not stored, and the check reports it.
73
-
74
- Declare one action per kind of email: the owner's notification and the client's confirmation are two actions, each with its own recipients and audience.
75
-
76
- ## Core pattern
77
-
78
- ```ts
79
- const submit = createServerFn({ method: 'POST' })
80
- .validator(contactSchema)
81
- .handler(async ({ data }) => {
82
- const saved = await ds.create(saveSubmission, { data })
83
- try {
84
- await email.send(notifyOwner, {
85
- subject: `New message from ${data.name}`,
86
- body: renderSubmission(data), // the app's own HTML, with what the visitor typed escaped
87
- replyTo: data.email,
88
- idempotencyKey: `submission-${saved.id}`,
89
- })
90
- } catch (error) {
91
- if (error instanceof EmailError) return { saved: true, emailed: false, reason: error.message }
92
- throw error
93
- }
94
- return { saved: true, emailed: true }
95
- })
96
- ```
97
-
98
- `email.send(action, init)` takes:
99
-
100
- - `subject` and `body`. Required. `body` is plain text or HTML. Escape what visitors typed before you put it into HTML.
101
- - `to`, `cc`, `bcc`, `replyTo`: each used only where the action does not fix it, and `cc` and `bcc` never when it fixes `to`. Each entry is one address. At most 50 recipients per email.
102
- - `attachments`: `{ name, url }` each, a file uploaded to this app (the Softr SDK `file-uploads` guidance); any other URL is refused with `INVALID_INPUT`. 10 MB in total.
103
- - `idempotencyKey`: the same key sends once, however often the call is repeated. Pass one derived from what the email is about, such as the record it announces, so a retried request never sends twice. When nothing is saved first, the form makes one key per submission (`crypto.randomUUID()` when the person starts submitting), sends it with the request and reuses it when they retry. At most 128 characters. Left out, every call is a new email, so a retry after `TIMEOUT` or a double click sends twice.
104
-
105
- It answers `{ messageId }` once the email is sent. The answer never lists the recipients.
106
-
107
- ## Failure modes
108
-
109
- **Not handling `EmailError`.** `email.send` throws an `EmailError`, a `StepError` from `@softr/sdk/server`, with `status`, `code` and `message` when the email was not sent, and never retries by itself. Catch it, keep what the request already saved, and show the person a line they can act on: "Saved, but the email could not be sent." The codes:
110
-
111
- | `code` | Meaning |
112
- | --- | --- |
113
- | `NOT_FOUND` | No such action, or the caller is not in its audience |
114
- | `INVALID_INPUT` | No subject or body, an entry or placeholder that is not exactly one address, more than 50 recipients, an attachment that is not one of the app's uploads, or an idempotency key over 128 characters |
115
- | `UNAVAILABLE` | Softr could not send it or could not be reached: no workflow credits left, Softr's address past its 30 emails today, an unconfirmed sender, or an outage |
116
- | `TIMEOUT` | No answer in time. It may still have been sent: retry only with the same key |
117
- | `RATE_LIMITED` | The app sent too often in the last minute or day. Tell the person to try again later |
118
-
119
- **Letting a mistyped address reach Softr.** An address the person typed (the reply-to of a contact form, a client's email) that is not one address is refused with `INVALID_INPUT`, which reads as a vague failure. Check its format in the validator and tell them which field is wrong.
120
-
121
- **Calling from client code.** `email` is server-only. Wrap the send in a `createServerFn` and call that from the form or the button.
122
-
123
- **Taking recipients from the request when they could be fixed.** A `to` the browser sends is a `to` any caller can change. Fix it in the declaration, or read it on the server from the record the email is about.
124
-
125
- **Placeholders in the call.** `{LOGGED_IN_USER:EMAIL}` is filled only in the declaration. In the call it is sent as written and fails.
126
-
127
- **Building a workflow for an email the app sends itself.** A "Run custom workflow" trigger plus a "Send email" step does the same at more cost to the user: a workflow to keep published. Use the email action; keep workflows for schedules and table events.