@softr/sdk 0.0.27 → 0.0.28

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.
package/dist/action.d.ts CHANGED
@@ -31,84 +31,41 @@ type ActionPermission = {
31
31
  */
32
32
  interface SoftrDatasources {}
33
33
  type SoftrDatasourceId = keyof SoftrDatasources extends never ? string : keyof SoftrDatasources & string;
34
- /** The read type one datasource returns. */
35
34
  type ReadOf<TDatasource$1> = TDatasource$1 extends keyof SoftrDatasources ? SoftrDatasources[TDatasource$1] extends {
36
35
  read: infer TRecord;
37
36
  } ? TRecord : never : never;
38
- /**
39
- * The DSv2 type name one field declares, as codegen emitted it: `"NUMBER"`, `"SELECT[]"`.
40
- * `never` for a field no augmentation describes.
41
- */
42
37
  type FieldTypeOf<TDatasource$1, TFieldId> = TDatasource$1 extends keyof SoftrDatasources ? SoftrDatasources[TDatasource$1] extends {
43
38
  fieldTypes: infer TTypes;
44
39
  } ? TFieldId extends keyof TTypes ? TTypes[TFieldId] : never : never : never;
45
- /** The write type one datasource accepts for one operation. */
46
40
  type WriteOf<TDatasource$1, TOperation$1> = TDatasource$1 extends keyof SoftrDatasources ? TOperation$1 extends keyof SoftrDatasources[TDatasource$1] ? SoftrDatasources[TDatasource$1][TOperation$1] : never : never;
47
- /** The field ids a write may name. Any string, until {@link SoftrDatasources} is declared. */
48
41
  type FieldIdOf<TDatasource$1, TOperation$1> = keyof SoftrDatasources extends never ? string : keyof WriteOf<TDatasource$1, TOperation$1> & string;
49
- /**
50
- * The field ids a read may name. Any string, until {@link SoftrDatasources} is declared.
51
- *
52
- * A read constrains against the record type rather than a write type, because a read may
53
- * name a field no write can: a formula or a lookup is on the record and on neither.
54
- */
55
42
  type ReadFieldIdOf<TDatasource$1> = keyof SoftrDatasources extends never ? string : keyof ReadOf<TDatasource$1> & string;
56
- /**
57
- * What an action declares, as the type layer reads it. `permission` is absent because no
58
- * call site depends on it, and the build reads it off the value.
59
- */
60
43
  type ActionShape = {
61
44
  id: string;
62
45
  datasource: SoftrDatasourceId;
63
46
  operation: ActionOperation;
64
47
  fields?: Record<string, string>;
65
48
  };
66
- /** What a read declares. A read is the whole field list, so `fields` is not optional. */
67
49
  type ReadShape = ActionShape & {
68
50
  operation: "read";
69
51
  fields: Record<string, string>;
70
52
  };
71
- /**
72
- * What one action writes: its own aliases, each resolved to the value that field accepts.
73
- *
74
- * An alias renames the field, and a mapped type keeps a property's docstring only while it
75
- * keeps the key, so the prose that sits on the field in `softr-env.d.ts` does not reach the
76
- * call site. The type does.
77
- */
78
53
  type ActionData<TShape extends ActionShape> = keyof SoftrDatasources extends never ? Record<string, unknown> : TShape extends {
79
54
  datasource: infer TDatasource;
80
55
  operation: infer TOperation;
81
56
  fields: infer TFields;
82
57
  } ? { [Alias in keyof TFields]?: WriteOf<TDatasource, TOperation>[TFields[Alias] & keyof WriteOf<TDatasource, TOperation>] } : Record<string, unknown>;
83
- /**
84
- * What a mutation reads back: the same aliases, resolved against the table's read type.
85
- *
86
- * A write returns the record it wrote, and the SDK aliases its fields on the way out, so a
87
- * field the action never declared is absent from the result as well as from the payload.
88
- *
89
- * Every alias the action does declare is present. An empty field reads as `null`, so nothing
90
- * here is optional.
91
- */
92
58
  type ActionRecord<TShape extends ActionShape> = keyof SoftrDatasources extends never ? Record<string, unknown> : TShape extends {
93
59
  datasource: infer TDatasource;
94
60
  fields: infer TFields;
95
61
  } ? { [Alias in keyof TFields]: ReadOf<TDatasource>[TFields[Alias] & keyof ReadOf<TDatasource>] } : Record<string, unknown>;
96
62
  declare const brand: unique symbol;
97
- /**
98
- * A handle to a declared action, and the only thing a mutation accepts. The brand keeps it
99
- * nominal, so an object literal of the same shape will not do and no mutation can be reached
100
- * without passing through a declaration the build reads.
101
- *
102
- * The brand carries the declaration itself, which is what lets a call site resolve what the
103
- * action writes without codegen having said anything about this action.
104
- */
105
63
  type Action<TShape extends ActionShape = ActionShape> = {
106
64
  type: "action";
107
65
  id: TShape["id"];
108
66
  } & {
109
67
  readonly [brand]: TShape;
110
68
  };
111
- /** What a write action declares. */
112
69
  type WriteDeclaration<TId extends string, TDatasource$1, TOperation$1, TFields$1> = {
113
70
  id: TId;
114
71
  datasource: TDatasource$1;
@@ -116,15 +73,12 @@ type WriteDeclaration<TId extends string, TDatasource$1, TOperation$1, TFields$1
116
73
  fields: TFields$1;
117
74
  permission: ActionPermission;
118
75
  };
119
- /** What a read action declares. */
120
76
  type ReadDeclaration<TId extends string, TDatasource$1, TFields$1> = {
121
77
  id: TId;
122
78
  datasource: TDatasource$1;
123
- operation: "read";
124
79
  fields: TFields$1;
125
80
  permission: ActionPermission;
126
81
  };
127
- /** What a delete action declares. `fields?: never` so a stray one is rejected rather than ignored. */
128
82
  type DeleteDeclaration<TId extends string, TDatasource$1> = {
129
83
  id: TId;
130
84
  datasource: TDatasource$1;
@@ -132,28 +86,15 @@ type DeleteDeclaration<TId extends string, TDatasource$1> = {
132
86
  fields?: never;
133
87
  permission: ActionPermission;
134
88
  };
135
- /** The whole declaration, which is what an action is at runtime and in the manifest. */
136
-
137
89
  /**
138
- * Declares a data operation: which datasource, which of the four operations, which fields
139
- * participate, and who may run it.
140
- *
141
- * One call per action, at module scope, with a literal: the build reads these declarations
142
- * to publish what the app may read and write.
90
+ * Declares a read: which datasource, which fields participate, and who may run it.
143
91
  *
144
- * The keys of `fields` are aliases the app chooses and the values are field ids of the
145
- * datasource the action names. The result, `data`, `where` and `orderBy` at the call site
146
- * are all keyed by those aliases, so nothing there restates what the action already says.
147
- *
148
- * A read fixes the table and the columns. It does not fix which rows: `where`, `orderBy` and
149
- * the paging arguments stay at the call site, because a list screen changes them as the user
150
- * types and a declaration cannot follow that.
92
+ * The argument must be a static object.
151
93
  *
152
94
  * @example
153
- * const publishedPosts = defineAction({
95
+ * const publishedPosts = defineQuery({
154
96
  * id: "publishedPosts",
155
97
  * datasource: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
156
- * operation: "read",
157
98
  * fields: { title: "V1StGXR8", author: "Z7pQm4Kd", publishedAt: "Xp2dLh6v" },
158
99
  * permission: { userGroup: "ALL_USERS" },
159
100
  * })
@@ -162,9 +103,21 @@ type DeleteDeclaration<TId extends string, TDatasource$1> = {
162
103
  * .list(publishedPosts)
163
104
  * .where((q) => q.publishedAt.lte(today))
164
105
  * .orderBy((q) => q.publishedAt.desc)
106
+ */
107
+ declare function defineQuery<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TFields$1 extends Record<string, ReadFieldIdOf<TDatasource$1>>>(definition: ReadDeclaration<TId, TDatasource$1, TFields$1>): Action<{
108
+ id: TId;
109
+ datasource: TDatasource$1;
110
+ operation: "read";
111
+ fields: TFields$1;
112
+ }>;
113
+ /**
114
+ * Declares a write: which datasource, whether it creates, updates or deletes, which fields
115
+ * participate, and who may run it.
116
+ *
117
+ * The argument must be a static object.
165
118
  *
166
119
  * @example
167
- * const closeOrderAction = defineAction({
120
+ * const closeOrder = defineMutation({
168
121
  * id: "closeOrder",
169
122
  * datasource: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
170
123
  * operation: "update",
@@ -172,24 +125,28 @@ type DeleteDeclaration<TId extends string, TDatasource$1> = {
172
125
  * permission: { userGroup: "LOGGED_IN_USERS", userGroupIds: ["grp_staff"] },
173
126
  * })
174
127
  *
175
- * await ds.update(closeOrderAction, { id, data: { status: "closed" } })
128
+ * await ds.update(closeOrder, { id, data: { status: "closed" } })
129
+ *
130
+ * @example
131
+ * const removeOrder = defineMutation({
132
+ * id: "removeOrder",
133
+ * datasource: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
134
+ * operation: "delete",
135
+ * permission: { userGroup: "LOGGED_IN_USERS", userGroupIds: ["grp_staff"] },
136
+ * })
137
+ *
138
+ * await ds.delete(removeOrder, { id })
176
139
  */
177
- declare function defineAction<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TFields$1 extends Record<string, ReadFieldIdOf<TDatasource$1>>>(definition: ReadDeclaration<TId, TDatasource$1, TFields$1>): Action<{
178
- id: TId;
179
- datasource: TDatasource$1;
180
- operation: "read";
181
- fields: TFields$1;
182
- }>;
183
- declare function defineAction<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TOperation$1 extends "create" | "update", const TFields$1 extends Record<string, FieldIdOf<TDatasource$1, TOperation$1>>>(definition: WriteDeclaration<TId, TDatasource$1, TOperation$1, TFields$1>): Action<{
140
+ declare function defineMutation<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TOperation$1 extends "create" | "update", const TFields$1 extends Record<string, FieldIdOf<TDatasource$1, TOperation$1>>>(definition: WriteDeclaration<TId, TDatasource$1, TOperation$1, TFields$1>): Action<{
184
141
  id: TId;
185
142
  datasource: TDatasource$1;
186
143
  operation: TOperation$1;
187
144
  fields: TFields$1;
188
145
  }>;
189
- declare function defineAction<const TId extends string, const TDatasource$1 extends SoftrDatasourceId>(definition: DeleteDeclaration<TId, TDatasource$1>): Action<{
146
+ declare function defineMutation<const TId extends string, const TDatasource$1 extends SoftrDatasourceId>(definition: DeleteDeclaration<TId, TDatasource$1>): Action<{
190
147
  id: TId;
191
148
  datasource: TDatasource$1;
192
149
  operation: "delete";
193
150
  }>;
194
151
  //#endregion
195
- export { Action, ActionData, ActionRecord, ActionShape, FieldTypeOf, ReadOf, ReadShape, SoftrDatasources, defineAction };
152
+ export { Action, ActionData, ActionRecord, ActionShape, FieldTypeOf, ReadOf, ReadShape, SoftrDatasources, defineMutation, defineQuery };
package/dist/action.js CHANGED
@@ -1,16 +1,17 @@
1
1
  //#region src/action.ts
2
- function defineAction(definition) {
2
+ function defineQuery(definition) {
3
+ return {
4
+ type: "action",
5
+ ...definition,
6
+ operation: "read"
7
+ };
8
+ }
9
+ function defineMutation(definition) {
3
10
  return {
4
11
  type: "action",
5
12
  ...definition
6
13
  };
7
14
  }
8
- /**
9
- * True for anything {@link defineAction} produced, whatever its operation.
10
- *
11
- * Matched on `type` rather than with `instanceof`, so two copies of the SDK in one
12
- * dependency tree still recognise each other's actions.
13
- */
14
15
  function isAction(value) {
15
16
  if (typeof value !== "object" || value === null) return false;
16
17
  const candidate = value;
@@ -18,4 +19,4 @@ function isAction(value) {
18
19
  }
19
20
 
20
21
  //#endregion
21
- export { defineAction, isAction };
22
+ export { defineMutation, defineQuery, isAction };
@@ -443,7 +443,7 @@ function nameOf(datasourceId, context) {
443
443
  }
444
444
  /**
445
445
  * Always quoted, even where an identifier would do. Find All References on a field id at a
446
- * `defineAction` call site matches string literals and quoted property names, so a bare key is
446
+ * `defineQuery` call site matches string literals and quoted property names, so a bare key is
447
447
  * invisible to it and the id no longer navigates here.
448
448
  */
449
449
  function key(name) {
@@ -1,5 +1,5 @@
1
1
  //#region src/core/integration.d.ts
2
- declare function integrationFetch(integrationId: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
2
+ declare function integrationFetch(datasourceId: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
3
3
  declare const integration: {
4
4
  fetch: typeof integrationFetch;
5
5
  };
@@ -2,7 +2,7 @@ import { readSessionToken } from "../auth/session.js";
2
2
  import { dsUrl } from "../datasource/record.js";
3
3
 
4
4
  //#region src/core/integration.ts
5
- async function integrationFetch(integrationId, input, init) {
5
+ async function integrationFetch(datasourceId, input, init) {
6
6
  const req = new Request(input, init);
7
7
  if (req.signal.aborted) throw req.signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
8
8
  const reqHeaders = {};
@@ -16,7 +16,7 @@ async function integrationFetch(integrationId, input, init) {
16
16
  };
17
17
  const method = req.method.toUpperCase();
18
18
  if (method !== "GET" && method !== "HEAD" && req.body !== null) payload.body = await req.clone().text();
19
- return fetch(`${dsUrl(integrationId)}/proxy`, {
19
+ return fetch(`${dsUrl(datasourceId)}/proxy`, {
20
20
  method: "POST",
21
21
  headers: {
22
22
  "Content-Type": "application/json",
@@ -232,7 +232,8 @@ async function deleteRecord(action, options) {
232
232
  if (!res.ok) throw new Error(`Softr datasource delete failed for "${datasourceId}": ${res.status}`);
233
233
  }
234
234
  function resolveTarget(subject, operation) {
235
- assert(isAction(subject), "a read or a mutation takes an action from defineAction");
235
+ const declaredBy = operation === "read" ? "defineQuery" : "defineMutation";
236
+ assert(isAction(subject), () => `Expected an action from ${declaredBy} but got ${JSON.stringify(subject)}`);
236
237
  const action = subject;
237
238
  assert(action.operation === operation, () => `action "${action.id}" is declared as ${action.operation}, not ${operation}`);
238
239
  return {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
2
- import { SoftrDatasources, defineAction } from "./action.js";
2
+ import { SoftrDatasources, defineMutation, defineQuery } from "./action.js";
3
3
  import { SoftrContext } from "./auth/context.js";
4
4
  import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
5
5
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
6
6
  import { useUserTimezone } from "./timezone.js";
7
7
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
8
- export { DefaultErrorOverlay, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, defineAction, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
8
+ export { DefaultErrorOverlay, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, defineMutation, defineQuery, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { defineAction } from "./action.js";
1
+ import { defineMutation, defineQuery } from "./action.js";
2
2
  import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
3
3
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
4
4
  import { useUserTimezone } from "./timezone.js";
5
5
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
6
6
 
7
- export { DefaultErrorOverlay, defineAction, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
7
+ export { DefaultErrorOverlay, defineMutation, defineQuery, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.27";
2
+ var version = "0.0.28";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: core
13
13
  library: softr-sdk
14
- library_version: 0.0.27
14
+ library_version: 0.0.28
15
15
  ---
16
16
 
17
17
  # Softr SDK core
@@ -16,7 +16,7 @@ description: >-
16
16
  metadata:
17
17
  type: sub-skill
18
18
  library: softr-sdk
19
- library_version: 0.0.27
19
+ library_version: 0.0.28
20
20
  ---
21
21
 
22
22
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.27
13
+ library_version: 0.0.28
14
14
  ---
15
15
 
16
16
  **Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.27
11
+ library_version: 0.0.28
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. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.27
12
+ library_version: 0.0.28
13
13
  ---
14
14
 
15
15
  **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -33,7 +33,7 @@ When the user attaches integrations to the chat, they appear in the `## Attached
33
33
  - "Gmail" — database `<databaseId>`, integration `<integrationId>`, type `GMAIL`
34
34
  ```
35
35
 
36
- Use `databaseId` as the first argument to `integration.fetch`. Use `integrationType` (e.g. `GMAIL`) to look up the base URL from the reference table below — then construct the full URL by appending the path and query parameters your training knows for that API.
36
+ Use `databaseId` as the first argument to `integration.fetch` — that is the `database` coordinate, not the `integration` one. The `integration` value is the OAuth connection identifier; do not pass it to `integration.fetch`. Use `integrationType` (e.g. `GMAIL`) to look up the base URL from the reference table below — then construct the full URL by appending the path and query parameters your training knows for that API.
37
37
 
38
38
  ## Core pattern
39
39
 
@@ -49,7 +49,7 @@ const getCalendarEvents = createServerFn().handler(async () => {
49
49
  })
50
50
  ```
51
51
 
52
- The id (`25d60213-…`) is the `databaseId` from the attached data sources section. The base URL (`https://www.googleapis.com`) is looked up from the reference table below by matching `integrationType` (`GOOGLE_CALENDAR`). The path and query params come from your knowledge of the Google Calendar REST API.
52
+ The id (`25d60213-…`) is the `databaseId` from the attached data sources section — the `database` coordinate, not the `integration` one. The base URL (`https://www.googleapis.com`) is looked up from the reference table below by matching `integrationType` (`GOOGLE_CALENDAR`). The path and query params come from your knowledge of the Google Calendar REST API.
53
53
 
54
54
  ## POST request
55
55
 
@@ -5,12 +5,12 @@ description: >-
5
5
  ds.get from @softr/sdk/server, the q query builder (q.text, q.number,
6
6
  q.boolean, q.date, q.array, q.and, q.or, q.asc, q.desc), q.select field
7
7
  aliasing, filtering, sorting, the 20-record default and 100-record cap, and
8
- nextOffset pagination. Reads only. Mutations, workflows, integrations, metrics
9
- and chart data are not covered.
8
+ nextOffset pagination. Reads only. Mutations, workflows, integrations, and
9
+ chart/metric aggregations (ds.chart, ds.metric) are not covered.
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.27
13
+ library_version: 0.0.28
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. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.27
15
+ library_version: 0.0.28
16
16
  ---
17
17
 
18
18
  **Boundary.** `defineVisibility`, the error predicates and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.