@softr/sdk 0.0.47-alpha.0 → 0.0.47

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.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.47-alpha.0
13
+ library_version: 0.0.47
14
14
  ---
15
15
 
16
16
  **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.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.47-alpha.0
13
+ library_version: 0.0.47
14
14
  ---
15
15
 
16
16
  **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.
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: mcp
3
+ description: >-
4
+ Open the app to AI assistants over MCP: softrMcpMiddleware({ actions }) from
5
+ @softr/sdk, registered in src/start.ts ahead of softrAuthMiddleware, turns the
6
+ actions you list into tools an assistant can call as the signed-in visitor.
7
+ The list is explicit — an action left out is invisible over MCP. The app's own
8
+ login must honour a `redirect` search param, or the approval step dead-ends.
9
+ metadata:
10
+ type: sub-skill
11
+ library: softr-sdk
12
+ library_version: 0.0.47
13
+ ---
14
+
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.
16
+
17
+ # MCP
18
+
19
+ An app can answer an AI assistant on its own origin. The assistant signs in as an ordinary app user, and the actions you list become the tools it may call. Not wired by default — a new app serves no MCP endpoint until you add the middleware.
20
+
21
+ ## The URL
22
+
23
+ The app's own public origin, plus `/_softr/mcp`:
24
+
25
+ ```
26
+ https://myapp.mysoftr.app/_softr/mcp
27
+ ```
28
+
29
+ Custom domain, default subdomain, a staging host — whatever origin the app is published on, the path is the same. That whole string is the MCP URL. The origin on its own is the app's home page, and an assistant pointed at it gets HTML back.
30
+
31
+ ## Setup
32
+
33
+ ```ts
34
+ // src/start.ts
35
+ import { createCsrfMiddleware, createStart } from '@tanstack/react-start'
36
+ import { softrAuthMiddleware, softrMcpMiddleware } from '@softr/sdk'
37
+ import { listCafes, addCafe } from './actions'
38
+
39
+ export const startInstance = createStart(() => ({
40
+ requestMiddleware: [
41
+ createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'serverFn' }),
42
+ softrMcpMiddleware({ actions: [listCafes, addCafe] }),
43
+ softrAuthMiddleware(),
44
+ ],
45
+ }))
46
+ ```
47
+
48
+ Order is part of the contract. `softrMcpMiddleware` goes **before** `softrAuthMiddleware`, and that is the only order it supports.
49
+
50
+ ## Choosing the actions
51
+
52
+ The list is the whole tool surface. Write it out:
53
+
54
+ - An action you leave out is invisible to the assistant, even where the app's pages use it freely.
55
+ - An action you list still obeys its own `permissions`. Listing it does not widen who may run it — the assistant runs as whoever signed in, with that user's groups.
56
+ - Two actions with the same `id` throw at startup. So does anything that is not a `defineQuery` or `defineMutation` value.
57
+
58
+ Reads first. Add a write only when the user asked for one, and name it back to them.
59
+
60
+ ## The login round trip
61
+
62
+ Connecting starts with an approval page the app serves. Most people hit it signed out, and that page cannot sign anyone in. It sends them to the app's sign-in page with a `redirect` search param holding where to come back to. That page is `/login` unless you pass `loginPath`:
63
+
64
+ ```ts
65
+ softrMcpMiddleware({ actions: [listCafes], loginPath: '/sign-in' })
66
+ ```
67
+
68
+ **The app's login has to honour that param.** Carry it through every exit that ends a sign-in, the second-factor step included, and send the visitor there instead of the home page. Drop it anywhere along the way and the approval dies quietly: they land on the home page, the assistant never gets its answer, nothing reports an error.
69
+
70
+ Validate it before following it. A path on this app and nothing else. Ask the URL parser rather than checking prefixes: resolve the value against a placeholder origin and reject it unless the origin survives. Browsers read `\` as `/` and drop tabs and newlines, so a value that does not start with `//` can still leave the app. The whole flow starts with a link someone was sent, so this is the one redirect an attacker can reliably aim.
71
+
72
+ The blank template does this in `src/routes/_auth/login.tsx`, `src/routes/verify.tsx` and `src/lib/after-auth.ts`. Read those before writing your own.
73
+
74
+ ## Failure modes
75
+
76
+ **Registering with no actions.** `softrMcpMiddleware()` is valid and serves an endpoint with nothing on it. The assistant connects, lists tools, finds only `whoami`. Pass the actions.
77
+
78
+ **Registering after `softrAuthMiddleware`.** Not a supported order. Put the MCP middleware first.
79
+
80
+ **Handing over the app's URL as the MCP URL.** The origin is where the app's pages are. Without `/_softr/mcp` on the end the assistant has nothing to talk to, and the owner is left comparing a URL that looks right against a connection that never works.
81
+
82
+ **A login that drops `redirect`.** Sign-in works, approval doesn't. The most common break, and silent — test the connect flow signed out, not only signed in.
83
+
84
+ **Following `redirect` unchecked, or checked by prefix.** Anything that is not a path on this app is someone else's page, and `/\evil.test` is one of them. Let the URL parser decide, and fall back to `/`.
85
+
86
+ ## References
87
+
88
+ - [record-queries/SKILL.md](../record-queries/SKILL.md): declaring the read actions you expose
89
+ - [record-mutations/SKILL.md](../record-mutations/SKILL.md): declaring the write actions you expose
90
+ - [auth/SKILL.md](../auth/SKILL.md): the sign-in and second-factor flows the round trip passes through
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: sub-skill
13
13
  library: softr-sdk
14
- library_version: 0.0.47-alpha.0
14
+ library_version: 0.0.47
15
15
  ---
16
16
 
17
17
  **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.
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.47-alpha.0
16
+ library_version: 0.0.47
17
17
  ---
18
18
 
19
19
  **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.47-alpha.0
12
+ library_version: 0.0.47
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.47-alpha.0
16
+ library_version: 0.0.47
17
17
  ---
18
18
 
19
19
  **Boundary.** `defineVisibility`, 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.47-alpha.0
12
+ library_version: 0.0.47
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.
package/dist/pwa.d.ts DELETED
@@ -1,49 +0,0 @@
1
- import * as react0 from "react";
2
- import "@tanstack/react-start";
3
-
4
- //#region src/pwa.d.ts
5
-
6
- /**
7
- * The links that make the app installable: its manifest and its home screen
8
- * icon. Render it once, anywhere in the root route; React moves the links into
9
- * `<head>`.
10
- *
11
- * @example
12
- * function RootComponent() {
13
- * return (
14
- * <>
15
- * <SoftrPwaHead />
16
- * <Outlet />
17
- * </>
18
- * )
19
- * }
20
- */
21
- declare function SoftrPwaHead(): react0.FunctionComponentElement<react0.FragmentProps>;
22
- type PwaPlatform = "ios" | "android" | "desktop";
23
- type PwaInstall = {
24
- available: false;
25
- } | {
26
- available: true;
27
- /** Which steps install the app on this device. */
28
- platform: PwaPlatform;
29
- /** The name the home screen will show. */
30
- name: string;
31
- /** The icon the home screen will show. */
32
- iconUrl: string;
33
- };
34
- /**
35
- * Whether to offer installing the app, and on which platform. Render an
36
- * install button only while this says `available`: it is unavailable while
37
- * loading, when the builder has not turned the PWA on, when Softr has switched
38
- * it off, and when the app already runs installed.
39
- *
40
- * @example
41
- * function InstallButton() {
42
- * const install = usePwaInstall()
43
- * if (!install.available) return null
44
- * return <button onClick={() => openSteps(install.platform)}>Install {install.name}</button>
45
- * }
46
- */
47
- declare function usePwaInstall(): PwaInstall;
48
- //#endregion
49
- export { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall };
package/dist/pwa.js DELETED
@@ -1,147 +0,0 @@
1
- import { logger } from "./logger.js";
2
- import { fetch } from "./fetch.js";
3
- import { sessionConfig } from "./auth/config.js";
4
- import { platformApiUrl } from "./auth/platform.js";
5
- import { Fragment, createElement, useEffect, useState } from "react";
6
- import { createMiddleware } from "@tanstack/react-start";
7
- import { z } from "zod";
8
-
9
- //#region src/pwa.ts
10
- const MANIFEST_PATH = "/manifest.webmanifest";
11
- const ICON_PATH = "/apple-touch-icon.png";
12
- const INSTALL_PATH = "/_softr/pwa.json";
13
- const TTL_MS = 6e4;
14
- const CACHE_CONTROL = "public, max-age=60";
15
- const pwaSchema = z.object({
16
- manifest: z.record(z.string(), z.unknown()),
17
- install: z.object({
18
- name: z.string(),
19
- iconUrl: z.string()
20
- })
21
- });
22
- let cached;
23
- /**
24
- * The app's PWA, or `null` when it is not installable right now. Never
25
- * rejects: a failure reads as not installable and is not remembered, so the
26
- * next request tries again.
27
- */
28
- function readPwa() {
29
- const now = Date.now();
30
- if (cached !== void 0 && cached.expires > now) return cached.pwa;
31
- const entry = {
32
- expires: now + TTL_MS,
33
- pwa: fetchPwa().catch((error) => {
34
- logger.warn("Could not read the PWA settings:", error);
35
- if (cached === entry) cached = void 0;
36
- return null;
37
- })
38
- };
39
- cached = entry;
40
- return entry.pwa;
41
- }
42
- async function fetchPwa() {
43
- const { applicationId } = sessionConfig();
44
- const response = await fetch(`${await platformApiUrl()}/v1/vibe/applications/${applicationId}/pwa`, { headers: { accept: "application/json" } });
45
- if (response.status === 404) return null;
46
- if (!response.ok) throw new Error(`The platform answered ${response.status}`);
47
- return pwaSchema.parse(await response.json());
48
- }
49
- /**
50
- * Serves the app's web app manifest, its home screen icon, and what an install
51
- * button needs to know, from the app's own origin. Every other request passes
52
- * through untouched.
53
- *
54
- * Answers 404, never an error page, when the app is not installable: a browser
55
- * silently drops a manifest it cannot parse, so a 500 there would be invisible.
56
- */
57
- function pwaMiddleware() {
58
- return createMiddleware({ type: "request" }).server(async ({ next, pathname }) => {
59
- if (pathname !== MANIFEST_PATH && pathname !== ICON_PATH && pathname !== INSTALL_PATH) return next();
60
- const pwa = await readPwa();
61
- if (pwa === null) return new Response(null, { status: 404 });
62
- if (pathname === MANIFEST_PATH) return new Response(JSON.stringify(pwa.manifest), { headers: {
63
- "content-type": "application/manifest+json",
64
- "cache-control": CACHE_CONTROL
65
- } });
66
- if (pathname === INSTALL_PATH) return Response.json(pwa.install, { headers: { "cache-control": CACHE_CONTROL } });
67
- try {
68
- const iconResponse = await fetch(pwa.install.iconUrl);
69
- if (!iconResponse.ok) return new Response(null, { status: 404 });
70
- return new Response(iconResponse.body, { headers: {
71
- "content-type": "image/png",
72
- "cache-control": CACHE_CONTROL
73
- } });
74
- } catch (error) {
75
- logger.warn("Could not fetch the PWA icon:", error);
76
- return new Response(null, { status: 404 });
77
- }
78
- });
79
- }
80
- /**
81
- * The links that make the app installable: its manifest and its home screen
82
- * icon. Render it once, anywhere in the root route; React moves the links into
83
- * `<head>`.
84
- *
85
- * @example
86
- * function RootComponent() {
87
- * return (
88
- * <>
89
- * <SoftrPwaHead />
90
- * <Outlet />
91
- * </>
92
- * )
93
- * }
94
- */
95
- function SoftrPwaHead() {
96
- return createElement(Fragment, null, createElement("link", {
97
- rel: "manifest",
98
- href: MANIFEST_PATH
99
- }), createElement("link", {
100
- rel: "apple-touch-icon",
101
- href: ICON_PATH
102
- }));
103
- }
104
- const installSchema = pwaSchema.shape.install;
105
- /**
106
- * Whether to offer installing the app, and on which platform. Render an
107
- * install button only while this says `available`: it is unavailable while
108
- * loading, when the builder has not turned the PWA on, when Softr has switched
109
- * it off, and when the app already runs installed.
110
- *
111
- * @example
112
- * function InstallButton() {
113
- * const install = usePwaInstall()
114
- * if (!install.available) return null
115
- * return <button onClick={() => openSteps(install.platform)}>Install {install.name}</button>
116
- * }
117
- */
118
- function usePwaInstall() {
119
- const [install, setInstall] = useState({ available: false });
120
- useEffect(() => {
121
- if (window.matchMedia("(display-mode: standalone)").matches || "standalone" in navigator && navigator.standalone === true) return;
122
- let cancelled = false;
123
- fetch(INSTALL_PATH, { headers: { accept: "application/json" } }).then(async (response) => {
124
- if (!response.ok) return;
125
- const parsed = installSchema.safeParse(await response.json());
126
- if (!parsed.success || cancelled) return;
127
- setInstall({
128
- available: true,
129
- platform: detectPlatform(),
130
- ...parsed.data
131
- });
132
- }).catch(() => {});
133
- return () => {
134
- cancelled = true;
135
- };
136
- }, []);
137
- return install;
138
- }
139
- function detectPlatform() {
140
- const userAgent = navigator.userAgent;
141
- if (/iPhone|iPad|iPod/.test(userAgent) || /Macintosh/.test(userAgent) && navigator.maxTouchPoints > 1) return "ios";
142
- if (/Android/.test(userAgent)) return "android";
143
- return "desktop";
144
- }
145
-
146
- //#endregion
147
- export { SoftrPwaHead, pwaMiddleware, usePwaInstall };
@@ -1,163 +0,0 @@
1
- ---
2
- name: pwa
3
- description: >-
4
- Let visitors install the app on their home screen: SoftrPwaHead for the
5
- manifest and icon links, usePwaInstall to decide whether to show an install
6
- button and which platform's steps to show, the PwaInstall and PwaPlatform
7
- types, how to check that the app is installable, and the default install
8
- button pattern. The manifest and icon are already served; never write them.
9
- metadata:
10
- type: sub-skill
11
- library: softr-sdk
12
- library_version: 0.0.47-alpha.0
13
- ---
14
-
15
- # Installing the app
16
-
17
- The builder turns installing on in Studio, under Settings → Mobile app, and sets the name, colour and icon there. The app already answers with its manifest and icon once they do. What the app adds is two things: the links that point the browser at them, and a button that tells visitors how to install.
18
-
19
- > **CRITICAL**: Never write a web app manifest, icon files, a service worker, or `manifest` / `apple-touch-icon` links of your own, in `public/` or in `head()`. The app already serves them from Studio's settings, and a file of your own shadows those settings.
20
- > **CRITICAL**: Render an install button only while `usePwaInstall()` returns `available: true`. Never show it unconditionally, never cache the answer, never replace the check with your own. Softr can switch installing off for an app, and this check is the only way that reaches the button.
21
-
22
- ## Setup
23
-
24
- ```ts
25
- import { SoftrPwaHead, usePwaInstall } from '@softr/sdk'
26
- import type { PwaInstall, PwaPlatform } from '@softr/sdk'
27
- ```
28
-
29
- ```ts
30
- type PwaPlatform = 'ios' | 'android' | 'desktop'
31
-
32
- type PwaInstall =
33
- | { available: false }
34
- | {
35
- available: true
36
- platform: PwaPlatform // which steps install the app on this device
37
- name: string // what the home screen will show
38
- iconUrl: string // the icon the home screen will show
39
- }
40
- ```
41
-
42
- ## Is the app installable?
43
-
44
- Ask the running app for `/_softr/pwa.json`. `200` means installable, and the body is the name and icon. `404` means the builder has not turned installing on, or has not uploaded an icon. Do not build an install button against a `404`: tell the builder what to turn on instead. A change in Studio can take a few minutes to reach the app.
45
-
46
- ## Core Patterns
47
-
48
- ### The links: `<SoftrPwaHead />`
49
-
50
- Render it once, in the root route's shell. React moves the links into `<head>`, so it can sit anywhere in the tree.
51
-
52
- ```tsx
53
- function RootDocument({ children }: { children: React.ReactNode }) {
54
- return (
55
- <html lang="en">
56
- <head>
57
- <HeadContent />
58
- </head>
59
- <body>
60
- <SoftrPwaHead />
61
- {children}
62
- <Scripts />
63
- </body>
64
- </html>
65
- )
66
- }
67
- ```
68
-
69
- ### Whether to show a button: `usePwaInstall()`
70
-
71
- It fetches after mount, so the first render, the server render included, always says `available: false`. It stays unavailable when the app is not installable, when the request fails, and when the app already runs installed from the home screen. Inside the Studio preview it says available, so the builder can see the button; installing itself only works on the published app.
72
-
73
- There is no `install()` to call. No browser opens its install dialog for this app on request, so the button opens instructions instead.
74
-
75
- ### The default button pattern
76
-
77
- Start from this unless the builder asked for something else. A round floating button with a download icon, placed where it covers nothing else on screen, that opens a sheet with the steps for the visitor's platform.
78
-
79
- ```tsx
80
- function InstallButton() {
81
- const install = usePwaInstall()
82
- const [open, setOpen] = useState(false)
83
- if (!install.available) return null
84
-
85
- return (
86
- <>
87
- <button
88
- type="button"
89
- aria-label={`Install ${install.name}`}
90
- onClick={() => setOpen(true)}
91
- className="fixed right-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] z-40 size-12 rounded-full"
92
- >
93
- <DownloadIcon />
94
- </button>
95
- {open ? <InstallSteps install={install} onClose={() => setOpen(false)} /> : null}
96
- </>
97
- )
98
- }
99
-
100
- function InstallSteps({
101
- install,
102
- onClose,
103
- }: {
104
- install: Extract<PwaInstall, { available: true }>
105
- onClose: () => void
106
- }) {
107
- return (
108
- <dialog open onClose={onClose}>
109
- <img src={install.iconUrl} alt="" width={64} height={64} />
110
- <h2>Install {install.name}</h2>
111
- {install.platform === 'ios' ? (
112
- <ol>
113
- <li>Tap the Share button in the browser's toolbar.</li>
114
- <li>Choose Add to Home Screen.</li>
115
- <li>Tap Add.</li>
116
- </ol>
117
- ) : (
118
- <ol>
119
- <li>Open the browser's menu.</li>
120
- <li>Choose Install app, or Add to Home screen.</li>
121
- </ol>
122
- )}
123
- </dialog>
124
- )
125
- }
126
- ```
127
-
128
- The markup is only the shape. Build it from the app's own components and design tokens so it looks like part of the app, and change its look, place or wording freely. What does not change is the `available` check that decides whether it renders at all.
129
-
130
- ## Common Mistakes
131
-
132
- ### Showing the button without the check
133
-
134
- Wrong:
135
-
136
- ```tsx
137
- function InstallButton() {
138
- const install = usePwaInstall()
139
- return <button onClick={openSteps}>Install</button>
140
- }
141
- ```
142
-
143
- Correct:
144
-
145
- ```tsx
146
- function InstallButton() {
147
- const install = usePwaInstall()
148
- if (!install.available) return null
149
- return <button onClick={openSteps}>Install {install.name}</button>
150
- }
151
- ```
152
-
153
- A button that shows unconditionally stays up when installing is off, on a device that already has the app installed, and inside the installed app itself.
154
-
155
- ### Adding a manifest file
156
-
157
- Wrong: creating `public/manifest.webmanifest`, `public/apple-touch-icon.png`, or a `head()` link to either.
158
-
159
- Correct: `<SoftrPwaHead />` once in the root. The builder changes the name and icon in Studio, and a file in the app would keep the old ones.
160
-
161
- ## References
162
-
163
- - [SKILL.md](../SKILL.md): the entrypoints and which one each export lives on