@softr/sdk 0.0.99-alpha.0 → 0.0.99-alpha.1

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.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: core
11
11
  library: softr-sdk
12
- library_version: 0.0.99-alpha.0
12
+ library_version: 0.0.98
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`, `usePushNotifications`, `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,13 +47,13 @@ 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) |
56
+ | Let visitors turn on push notifications | [push-notifications/SKILL.md](./push-notifications/SKILL.md) |
59
57
  | Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
60
58
 
61
59
  ## Quick Decision Tree
@@ -79,9 +77,6 @@ Drawing a chart, a stat tile, or any aggregated number?
79
77
  Submitting a form, editing a record, or deleting one?
80
78
  load softr-sdk/record-mutations
81
79
 
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
80
  Triggering a Softr workflow from a button or form?
86
81
  load softr-sdk/workflows
87
82
 
@@ -103,6 +98,9 @@ Opening the app to an AI assistant over MCP?
103
98
  Adding an install button, or making the app installable?
104
99
  load softr-sdk/pwa
105
100
 
101
+ Letting visitors turn on push notifications, or "notify users when …"?
102
+ load softr-sdk/push-notifications
103
+
106
104
  Showing a date, a time or a number on a page?
107
105
  load softr-sdk/formatting
108
106
 
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.99-alpha.0
16
+ library_version: 0.0.98
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.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.99-alpha.0
12
+ library_version: 0.0.98
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.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.99-alpha.0
12
+ library_version: 0.0.98
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.99-alpha.0
12
+ library_version: 0.0.98
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.99-alpha.0
10
+ library_version: 0.0.98
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.99-alpha.0
11
+ library_version: 0.0.98
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.99-alpha.0
10
+ library_version: 0.0.98
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.99-alpha.0
12
+ library_version: 0.0.98
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.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: push-notifications
3
+ description: >-
4
+ Let signed-in visitors turn on push notifications: usePushNotifications for
5
+ the prompt, its states and the Studio texts, the PushNotifications and
6
+ PushPrompt types, and how to check that push is on for the app. Notifications
7
+ are sent by Softr Workflows, never by app code; the service worker is already
8
+ served, never write one.
9
+ metadata:
10
+ type: sub-skill
11
+ library: softr-sdk
12
+ library_version: 0.0.97
13
+ ---
14
+
15
+ # Push notifications
16
+
17
+ The builder turns push on in Studio, under Users → Notifications, for desktop, mobile or both, and writes the prompt's texts there. Notifications are sent by a Softr Workflow's "Send notification" action, which the builder sets up in Studio. What the app adds is one thing: a prompt that lets the signed-in visitor turn notifications on in this browser.
18
+
19
+ > **CRITICAL**: Never write a service worker, a `public/sw.js`, or any `navigator.serviceWorker`, `PushManager` or `Notification` code of your own. The app already serves its service worker, and `usePushNotifications()` registers it, subscribes and unsubscribes.
20
+ >
21
+ > **CRITICAL**: Never send a push from app code. Sending is a Softr Workflow's "Send notification" action. When the builder asks for "notify X when Y", point them to that workflow; when Y is something the app does, the app triggers the workflow (load the Softr SDK `workflows` guidance). The app itself adds only the prompt.
22
+
23
+ ## Setup
24
+
25
+ ```ts
26
+ import { usePushNotifications } from '@softr/sdk'
27
+ import type { PushNotifications, PushPrompt } from '@softr/sdk'
28
+ ```
29
+
30
+ ```ts
31
+ type PushPrompt = {
32
+ title: string | null // what the builder wrote in Studio; any of these may be missing
33
+ message: string | null
34
+ enableButtonLabel: string | null
35
+ }
36
+
37
+ type PushNotifications =
38
+ | { status: 'unavailable' } // loading, signed out, push off, this device type off, or no browser support
39
+ | { status: 'install-first'; prompt: PushPrompt } // iPhone and iPad: push works only in the installed app
40
+ | {
41
+ status: 'ask'
42
+ prompt: PushPrompt
43
+ dismissed: boolean // skipped recently, within the builder's skip days; hide the prompt
44
+ subscribe: () => Promise<void> // call from a click
45
+ dismiss: () => void
46
+ }
47
+ | { status: 'subscribed'; unsubscribe: () => Promise<void> }
48
+ | { status: 'blocked' } // blocked in the browser; only the visitor can undo it there
49
+ ```
50
+
51
+ ## Is push on for the app?
52
+
53
+ Ask the running app for `/_softr/push.json`: `200` means push is on, `404` means it is off in Studio or the plan does not include it. Do not build a prompt against a `404`: push has to be turned on first. A change in Studio can take up to two minutes to reach the app.
54
+
55
+ Push works only on the published app. Inside the Studio preview the hook always says `unavailable`, so the prompt never shows there. That is expected, not a bug to fix.
56
+
57
+ ## Core Patterns
58
+
59
+ ### The prompt
60
+
61
+ Render it once, in `RootDocument` in `src/routes/__root.tsx`, so it shows on every page and the browser stays registered for the visitor. Use the prompt component the template ships when it has one. It already handles each status.
62
+
63
+ Call `usePushNotifications()` in that one component only. Each call keeps state of its own: a second call, in a header or on a settings page, does not see a subscribe made through the first, so the prompt would stay on screen after the visitor said yes.
64
+
65
+ `subscribe()` rejects when the browser or Softr refuses. Catch it and tell the visitor, so the button never fails in silence.
66
+
67
+ ```tsx
68
+ function EnableNotifications() {
69
+ const push = usePushNotifications()
70
+ if (push.status !== 'ask' || push.dismissed) return null
71
+ return (
72
+ <div role="dialog">
73
+ <p>{push.prompt.title ?? 'Turn on notifications'}</p>
74
+ <p>{push.prompt.message ?? 'Get notified about updates.'}</p>
75
+ <button onClick={() => push.subscribe().catch(() => toast.error('Notifications could not be turned on.'))}>
76
+ {push.prompt.enableButtonLabel ?? 'Turn on'}
77
+ </button>
78
+ <button onClick={push.dismiss}>Not now</button>
79
+ </div>
80
+ )
81
+ }
82
+ ```
83
+
84
+ Fall back to your own wording only where the builder left a text empty. On `install-first`, point the visitor to installing the app (load the Softr SDK `pwa` guidance); do not ask for permission.
85
+
86
+ ### Signing out
87
+
88
+ Nothing to add. The browser is unsubscribed when the visitor signs out, so the next person on the device gets nothing meant for them.
89
+
90
+ ## Common Mistakes
91
+
92
+ ### CRITICAL Asking for permission on page load
93
+
94
+ Wrong:
95
+
96
+ ```tsx
97
+ useEffect(() => { push.status === 'ask' && push.subscribe() }, [push.status])
98
+ ```
99
+
100
+ Correct:
101
+
102
+ ```tsx
103
+ <button onClick={() => push.subscribe().catch(() => toast.error('Notifications could not be turned on.'))}>
104
+ {push.prompt.enableButtonLabel ?? 'Turn on'}
105
+ </button>
106
+ ```
107
+
108
+ Browsers ignore or block a permission request that no click started, and a visitor who blocks it can only undo that in the browser's settings.
109
+
110
+ Source: packages/sdk/src/push.ts
111
+
112
+ ### HIGH Showing the prompt whatever the status
113
+
114
+ Wrong:
115
+
116
+ ```tsx
117
+ const push = usePushNotifications()
118
+ return <button onClick={() => push.subscribe()}>Turn on notifications</button>
119
+ ```
120
+
121
+ Correct:
122
+
123
+ ```tsx
124
+ const push = usePushNotifications()
125
+ if (push.status !== 'ask' || push.dismissed) return null
126
+ return (
127
+ <button onClick={() => push.subscribe().catch(() => toast.error('Notifications could not be turned on.'))}>
128
+ {push.prompt.enableButtonLabel ?? 'Turn on'}
129
+ </button>
130
+ )
131
+ ```
132
+
133
+ The hook says `unavailable` when push is off in Studio, for this device type, or for a signed-out visitor. A prompt that skips the check shows in all of those, and on iPhone it asks for something the browser cannot give.
134
+
135
+ Source: packages/sdk/src/push.ts
136
+
137
+ ## References
138
+
139
+ - [SKILL.md](../SKILL.md): the entrypoints and which one each export lives on
140
+ - [pwa/SKILL.md](../pwa/SKILL.md): installing the app, which iPhone and iPad need before push
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.99-alpha.0
12
+ library_version: 0.0.98
13
13
  ---
14
14
 
15
15
  # Installing the app
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.99-alpha.0
12
+ library_version: 0.0.98
13
13
  ---
14
14
 
15
15
  **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.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.99-alpha.0
13
+ library_version: 0.0.98
14
14
  ---
15
15
 
16
16
  **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.99-alpha.0
12
+ library_version: 0.0.98
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.99-alpha.0
12
+ library_version: 0.0.98
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.99-alpha.0
12
+ library_version: 0.0.98
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,48 +0,0 @@
1
- import { EmailAction } from "../action.js";
2
-
3
- //#region src/core/email.d.ts
4
- declare const EMAIL_ERROR_CODES: readonly ["NOT_FOUND", "INVALID_INPUT", "PLAN_RESTRICTED", "CREDITS_EXHAUSTED", "RATE_LIMITED", "DAILY_LIMIT_REACHED", "SENDER_UNAVAILABLE", "PROVIDER_REJECTED", "UNAVAILABLE", "TIMEOUT"];
5
- type EmailErrorCode = (typeof EMAIL_ERROR_CODES)[number];
6
- interface EmailSendInit {
7
- /** Ignored when the action declares its own. */
8
- to?: ReadonlyArray<string>;
9
- /** Ignored when the action declares its own `cc` or `to`. */
10
- cc?: ReadonlyArray<string>;
11
- /** Ignored when the action declares its own `bcc` or `to`. */
12
- bcc?: ReadonlyArray<string>;
13
- /** Ignored when the action declares its own. */
14
- replyTo?: string;
15
- subject: string;
16
- /** Plain text or HTML. */
17
- body: string;
18
- }
19
- interface EmailSent {
20
- /** Null when the mail provider did not report one; the email was still sent. */
21
- messageId: string | null;
22
- }
23
- /**
24
- * Thrown when Softr did not send the email. `code` says why:
25
- *
26
- * - `NOT_FOUND`: no such action, or the caller is not in its audience.
27
- * - `INVALID_INPUT`: the call's inputs were refused; the message says which.
28
- * - `PLAN_RESTRICTED`: the workspace's plan does not include sending this email.
29
- * - `CREDITS_EXHAUSTED`: the workspace has no workflow credits left.
30
- * - `RATE_LIMITED`: the app sent too many emails lately; try again later.
31
- * - `DAILY_LIMIT_REACHED`: Softr's own address sent the workspace's emails for today.
32
- * - `SENDER_UNAVAILABLE`: the chosen sender is missing or not confirmed, or the action leaves `to` open and would send from Softr's address.
33
- * - `PROVIDER_REJECTED`: the mail provider refused the email.
34
- * - `UNAVAILABLE`: Softr could not send it right now, or could not be reached.
35
- * - `TIMEOUT`: no answer in time. It may still have been sent.
36
- */
37
- declare class EmailError extends Error {
38
- readonly status: number;
39
- readonly code: EmailErrorCode;
40
- constructor(status: number, code: EmailErrorCode, message: string);
41
- }
42
- /** Sends one email through the action. Never retries by itself. */
43
- declare function sendEmail(action: EmailAction, init: EmailSendInit): Promise<EmailSent>;
44
- declare const email: {
45
- send: typeof sendEmail;
46
- };
47
- //#endregion
48
- export { EmailError, EmailErrorCode, EmailSendInit, EmailSent, email };
@@ -1,100 +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
-
7
- //#region src/core/email.ts
8
- /** Longer than Softr waits for the send itself, so its own timeout answers first. */
9
- const TIMEOUT_MS = 12e4;
10
- const EMAIL_ERROR_CODES = [
11
- "NOT_FOUND",
12
- "INVALID_INPUT",
13
- "PLAN_RESTRICTED",
14
- "CREDITS_EXHAUSTED",
15
- "RATE_LIMITED",
16
- "DAILY_LIMIT_REACHED",
17
- "SENDER_UNAVAILABLE",
18
- "PROVIDER_REJECTED",
19
- "UNAVAILABLE",
20
- "TIMEOUT"
21
- ];
22
- /**
23
- * Thrown when Softr did not send the email. `code` says why:
24
- *
25
- * - `NOT_FOUND`: no such action, or the caller is not in its audience.
26
- * - `INVALID_INPUT`: the call's inputs were refused; the message says which.
27
- * - `PLAN_RESTRICTED`: the workspace's plan does not include sending this email.
28
- * - `CREDITS_EXHAUSTED`: the workspace has no workflow credits left.
29
- * - `RATE_LIMITED`: the app sent too many emails lately; try again later.
30
- * - `DAILY_LIMIT_REACHED`: Softr's own address sent the workspace's emails for today.
31
- * - `SENDER_UNAVAILABLE`: the chosen sender is missing or not confirmed, or the action leaves `to` open and would send from Softr's address.
32
- * - `PROVIDER_REJECTED`: the mail provider refused the email.
33
- * - `UNAVAILABLE`: Softr could not send it right now, or could not be reached.
34
- * - `TIMEOUT`: no answer in time. It may still have been sent.
35
- */
36
- var EmailError = class extends Error {
37
- status;
38
- code;
39
- constructor(status, code, message) {
40
- super(message);
41
- this.name = "EmailError";
42
- this.status = status;
43
- this.code = code;
44
- }
45
- };
46
- /** Sends one email through the action. Never retries by itself. */
47
- async function sendEmail(action, init) {
48
- const { to, cc, bcc, replyTo, subject, body } = init;
49
- const sessionToken = readSessionToken();
50
- let res;
51
- try {
52
- res = await fetch(`${softrApiUrl("SOFTR_PLATFORM_API_URL")}/v1/vibe/applications/${sessionConfig().applicationId}/actions/${action.id}/email`, {
53
- method: "POST",
54
- headers: {
55
- ...sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {},
56
- "Content-Type": "application/json"
57
- },
58
- body: JSON.stringify({ input: {
59
- to,
60
- cc,
61
- bcc,
62
- replyTo,
63
- subject,
64
- body
65
- } }),
66
- signal: AbortSignal.timeout(TIMEOUT_MS)
67
- });
68
- } catch (error) {
69
- 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.`);
70
- throw new EmailError(502, "UNAVAILABLE", `Email "${action.id}" could not reach Softr: ${error instanceof Error ? error.message : String(error)}`);
71
- }
72
- if (res.ok) {
73
- const sent = await res.json();
74
- assert(typeof sent === "object" && sent !== null && "messageId" in sent && (typeof sent.messageId === "string" || sent.messageId === null), "the send answered without a messageId");
75
- return { messageId: sent.messageId };
76
- }
77
- const { code, message } = await readFailure(res, `Email "${action.id}" failed with HTTP ${res.status}`);
78
- throw new EmailError(res.status, code, message);
79
- }
80
- /** The code and message of a send Softr refused, read from its answer; `fallbackMessage` when it sent none. */
81
- async function readFailure(res, fallbackMessage) {
82
- const failure = await res.json().catch(() => null);
83
- const known = typeof failure === "object" && failure !== null ? failure : {};
84
- return {
85
- code: ("code" in known ? EMAIL_ERROR_CODES.find((errorCode) => errorCode === known.code) : void 0) ?? codeOfStatus(res.status),
86
- message: "message" in known && typeof known.message === "string" && known.message !== "" ? known.message : fallbackMessage
87
- };
88
- }
89
- function codeOfStatus(status) {
90
- switch (status) {
91
- case 400: return "INVALID_INPUT";
92
- case 404: return "NOT_FOUND";
93
- case 429: return "RATE_LIMITED";
94
- default: return "UNAVAILABLE";
95
- }
96
- }
97
- const email = { send: sendEmail };
98
-
99
- //#endregion
100
- export { EmailError, email };