@softr/sdk 0.0.69 → 0.0.71
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/auth/methods/profile.d.ts +26 -0
- package/dist/auth/methods/profile.js +52 -0
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/platform.js +9 -2
- package/dist/auth/server.d.ts +2 -0
- package/dist/auth/server.js +2 -0
- package/dist/auth/session.js +11 -1
- package/dist/auth/user.d.ts +4 -0
- package/dist/auth/user.js +3 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/package.js +1 -1
- package/dist/{visibility → route}/audience.d.ts +1 -1
- package/dist/{visibility → route}/audience.js +1 -1
- package/dist/route/page.d.ts +72 -0
- package/dist/route/page.js +79 -0
- package/dist/{visibility → route}/route.d.ts +13 -7
- package/dist/{visibility → route}/route.js +13 -9
- package/dist/{visibility → route}/visibility.d.ts +5 -10
- package/dist/{visibility → route}/visibility.js +12 -13
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/auth/SKILL.md +29 -10
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +3 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { User } from "../user.js";
|
|
2
|
+
import { AuthError, Result } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/methods/profile.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Changes the signed-in visitor's own name, photo, or both. A field left out
|
|
8
|
+
* stays as it is, and `avatar: null` removes the photo.
|
|
9
|
+
*
|
|
10
|
+
* The photo can change only where `user.avatarEditable` is true. Elsewhere a
|
|
11
|
+
* photo answers `avatarNotSupported`, so hide the photo controls instead.
|
|
12
|
+
*
|
|
13
|
+
* Answers with the visitor as they are now. The page keeps showing what it
|
|
14
|
+
* loaded until its route context loads again, so call `router.invalidate()`
|
|
15
|
+
* after a success.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const result = await auth.updateProfile({ fullName: 'Ann Lee' })
|
|
19
|
+
* const result = await auth.updateProfile({ avatar: file })
|
|
20
|
+
*/
|
|
21
|
+
declare function updateProfile(changes: {
|
|
22
|
+
fullName?: string;
|
|
23
|
+
avatar?: File | null;
|
|
24
|
+
}): Promise<Result<User, AuthError>>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { updateProfile };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { assert, panic } from "../../assertions.js";
|
|
2
|
+
import { sessionConfig } from "../config.js";
|
|
3
|
+
import { clearSessionData, readSessionToken } from "../session.js";
|
|
4
|
+
import { makePlatform } from "../platform.js";
|
|
5
|
+
import { authError } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
//#region src/auth/methods/profile.ts
|
|
8
|
+
/**
|
|
9
|
+
* Changes the signed-in visitor's own name, photo, or both. A field left out
|
|
10
|
+
* stays as it is, and `avatar: null` removes the photo.
|
|
11
|
+
*
|
|
12
|
+
* The photo can change only where `user.avatarEditable` is true. Elsewhere a
|
|
13
|
+
* photo answers `avatarNotSupported`, so hide the photo controls instead.
|
|
14
|
+
*
|
|
15
|
+
* Answers with the visitor as they are now. The page keeps showing what it
|
|
16
|
+
* loaded until its route context loads again, so call `router.invalidate()`
|
|
17
|
+
* after a success.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const result = await auth.updateProfile({ fullName: 'Ann Lee' })
|
|
21
|
+
* const result = await auth.updateProfile({ avatar: file })
|
|
22
|
+
*/
|
|
23
|
+
async function updateProfile(changes) {
|
|
24
|
+
assert(changes.fullName !== void 0 || changes.avatar !== void 0, "updateProfile needs a name or a photo to change");
|
|
25
|
+
const token = readSessionToken();
|
|
26
|
+
if (token === null) return authError("notAuthenticated", "Nobody is signed in.");
|
|
27
|
+
const form = new FormData();
|
|
28
|
+
if (changes.fullName !== void 0) form.set("fullName", changes.fullName);
|
|
29
|
+
if (changes.avatar === null) form.set("removeAvatar", "true");
|
|
30
|
+
if (changes.avatar) form.set("avatar", changes.avatar);
|
|
31
|
+
const result = await makePlatform(sessionConfig().applicationId).updateProfile(token, form);
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
switch (result.status) {
|
|
34
|
+
case 400: return authError("invalidName", result.message);
|
|
35
|
+
case 401: return authError("notAuthenticated", result.message);
|
|
36
|
+
case 409: return authError("avatarNotSupported", result.message);
|
|
37
|
+
case 413: return authError("fileTooLarge", result.message);
|
|
38
|
+
case 415: return authError("notAnImage", result.message);
|
|
39
|
+
case 429: return authError("tooManyAttempts", result.message);
|
|
40
|
+
}
|
|
41
|
+
if (result.status >= 500) panic(`Expected the profile to update, but got ${result.status}: ` + result.message);
|
|
42
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID.`);
|
|
43
|
+
}
|
|
44
|
+
clearSessionData();
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
data: result.data
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { updateProfile };
|
|
@@ -10,6 +10,6 @@ type AuthError = {
|
|
|
10
10
|
code: AuthErrorCode;
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
|
-
type AuthErrorCode = "invalidCredentials" | "tooManyAttempts" | "emailTaken" | "invalidDetails" | "invalidCode" | "notAuthenticated" | "emailDomainNotAllowed" | "linkExpired";
|
|
13
|
+
type AuthErrorCode = "invalidCredentials" | "tooManyAttempts" | "emailTaken" | "invalidDetails" | "invalidCode" | "notAuthenticated" | "emailDomainNotAllowed" | "linkExpired" | "invalidName" | "fileTooLarge" | "notAnImage" | "avatarNotSupported";
|
|
14
14
|
//#endregion
|
|
15
15
|
export { AuthError, Result };
|
package/dist/auth/platform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { panic } from "../assertions.js";
|
|
1
|
+
import { assert, panic } from "../assertions.js";
|
|
2
2
|
import { fetch } from "../fetch.js";
|
|
3
3
|
import { permissionsSchema } from "./permissions.js";
|
|
4
4
|
import { sessionConfig } from "./config.js";
|
|
@@ -267,6 +267,11 @@ function makePlatform(applicationId) {
|
|
|
267
267
|
token,
|
|
268
268
|
json: { code }
|
|
269
269
|
}, z.null()),
|
|
270
|
+
updateProfile: (token, form) => request(`/v1/vibe/applications/${applicationId}/users/me/profile`, {
|
|
271
|
+
method: "PATCH",
|
|
272
|
+
token,
|
|
273
|
+
form
|
|
274
|
+
}, userSchema),
|
|
270
275
|
signOut: (token) => request(`/v1/applications/${applicationId}/users/sign-out`, {
|
|
271
276
|
method: "POST",
|
|
272
277
|
token
|
|
@@ -275,6 +280,7 @@ function makePlatform(applicationId) {
|
|
|
275
280
|
}
|
|
276
281
|
const errorSchema = z.object({ message: z.string() });
|
|
277
282
|
async function request(path, init, schema, parse = (response$1) => response$1.json()) {
|
|
283
|
+
assert(init.json === void 0 || init.form === void 0, "A platform request carries a JSON body or a form, not both");
|
|
278
284
|
const response = await fetch(`${await platformApiUrl()}${path}`, {
|
|
279
285
|
method: init.method,
|
|
280
286
|
headers: {
|
|
@@ -282,7 +288,8 @@ async function request(path, init, schema, parse = (response$1) => response$1.js
|
|
|
282
288
|
...init.json === void 0 ? {} : { "content-type": "application/json" },
|
|
283
289
|
...init.token ? { authorization: `Bearer ${init.token}` } : {}
|
|
284
290
|
},
|
|
285
|
-
...init.json === void 0 ? {} : { body: JSON.stringify(init.json) }
|
|
291
|
+
...init.json === void 0 ? {} : { body: JSON.stringify(init.json) },
|
|
292
|
+
...init.form === void 0 ? {} : { body: init.form }
|
|
286
293
|
});
|
|
287
294
|
if (!response.ok) {
|
|
288
295
|
const error = errorSchema.safeParse(await response.json().catch(() => null));
|
package/dist/auth/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./
|
|
|
6
6
|
import { settings } from "./methods/settings.js";
|
|
7
7
|
import { confirmSignUp, signUp } from "./methods/signup.js";
|
|
8
8
|
import { signInWithGoogle } from "./methods/social.js";
|
|
9
|
+
import { updateProfile } from "./methods/profile.js";
|
|
9
10
|
import { currentUserRow } from "./current-user-row.js";
|
|
10
11
|
import "@tanstack/react-start/server-only";
|
|
11
12
|
|
|
@@ -34,6 +35,7 @@ declare const auth: {
|
|
|
34
35
|
resetAuthenticator: typeof resetAuthenticator;
|
|
35
36
|
removeAuthenticator: typeof removeAuthenticator;
|
|
36
37
|
settings: typeof settings;
|
|
38
|
+
updateProfile: typeof updateProfile;
|
|
37
39
|
currentUserRow: typeof currentUserRow;
|
|
38
40
|
};
|
|
39
41
|
//#endregion
|
package/dist/auth/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { signIn, signOut } from "./methods/password.js";
|
|
|
6
6
|
import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./methods/passwordLink.js";
|
|
7
7
|
import { confirmSignUp, signUp } from "./methods/signup.js";
|
|
8
8
|
import { signInWithGoogle } from "./methods/social.js";
|
|
9
|
+
import { updateProfile } from "./methods/profile.js";
|
|
9
10
|
import { currentUserRow } from "./current-user-row.js";
|
|
10
11
|
import "@tanstack/react-start/server-only";
|
|
11
12
|
|
|
@@ -33,6 +34,7 @@ const auth = {
|
|
|
33
34
|
resetAuthenticator,
|
|
34
35
|
removeAuthenticator,
|
|
35
36
|
settings,
|
|
37
|
+
updateProfile,
|
|
36
38
|
currentUserRow
|
|
37
39
|
};
|
|
38
40
|
|
package/dist/auth/session.js
CHANGED
|
@@ -118,6 +118,16 @@ function clearSession() {
|
|
|
118
118
|
PENDING_AUTH
|
|
119
119
|
]);
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Drops the cached visitor and leaves the session signed in, so the next
|
|
123
|
+
* request needing a visitor re-reads one from the platform.
|
|
124
|
+
*
|
|
125
|
+
* For a mutation to what the visitor's own record holds — their name, their
|
|
126
|
+
* groups — which the cache would otherwise answer stale for up to five minutes.
|
|
127
|
+
*/
|
|
128
|
+
function clearSessionData() {
|
|
129
|
+
clearCookies([SESSION_DATA]);
|
|
130
|
+
}
|
|
121
131
|
const standInToken = new AsyncLocalStorage();
|
|
122
132
|
const sessionDataSchema = z.object({
|
|
123
133
|
user: userSchema.nullable(),
|
|
@@ -210,4 +220,4 @@ function openEnvelope(raw, purpose) {
|
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
//#endregion
|
|
213
|
-
export { clearPendingAuth, clearSession, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken, readSessionData, readSessionToken, withSessionToken };
|
|
223
|
+
export { clearPendingAuth, clearSession, clearSessionData, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken, readSessionData, readSessionToken, withSessionToken };
|
package/dist/auth/user.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ type User = {
|
|
|
19
19
|
email: string;
|
|
20
20
|
fullName: string;
|
|
21
21
|
userGroupIds: Array<SoftrUserGroupId>;
|
|
22
|
+
/** Their photo, or `null` when they have none. Show their initials then. */
|
|
23
|
+
avatarUrl: string | null;
|
|
24
|
+
/** Whether `auth.updateProfile` can change their photo in this app. */
|
|
25
|
+
avatarEditable: boolean;
|
|
22
26
|
};
|
|
23
27
|
//#endregion
|
|
24
28
|
export { SoftrUserGroupId, SoftrUserGroups, User };
|
package/dist/auth/user.js
CHANGED
|
@@ -5,7 +5,9 @@ const userSchema = z.object({
|
|
|
5
5
|
id: z.string(),
|
|
6
6
|
email: z.string(),
|
|
7
7
|
fullName: z.string(),
|
|
8
|
-
userGroupIds: z.array(z.string())
|
|
8
|
+
userGroupIds: z.array(z.string()),
|
|
9
|
+
avatarUrl: z.string().nullable().default(null),
|
|
10
|
+
avatarEditable: z.boolean().default(false)
|
|
9
11
|
});
|
|
10
12
|
|
|
11
13
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
|
5
5
|
import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
6
6
|
import { SoftrContext } from "./auth/context.js";
|
|
7
7
|
import { AuthSettings } from "./auth/platform.js";
|
|
8
|
-
import { checkAccess, defineVisibility, isAuthorized } from "./
|
|
9
|
-
import {
|
|
8
|
+
import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
|
|
9
|
+
import { Page, PageSettings, definePage, pageHead } from "./route/page.js";
|
|
10
|
+
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
|
|
10
11
|
import { toReturnPath } from "./auth/returnPath.js";
|
|
11
12
|
import { AuthOptions, AuthSettingsProvider, getAuthSettings, getSoftrContext, initialSoftrContext, softrAuthMiddleware, useAuthSettings, useCurrentUser } from "./auth/isomorphic.js";
|
|
12
13
|
import { softrMcpMiddleware } from "./mcp.js";
|
|
@@ -15,5 +16,5 @@ import { useOverrides } from "./core/use-overrides.js";
|
|
|
15
16
|
import { useUserTimezone } from "./timezone.js";
|
|
16
17
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
17
18
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
18
|
-
import { Audience } from "./
|
|
19
|
-
export { type ActionPermissions, 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 PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
19
|
+
import { Audience } from "./route/audience.js";
|
|
20
|
+
export { type ActionPermissions, 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 User, type WeekDay, assertRouteAccess, checkAccess, 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,7 +1,8 @@
|
|
|
1
1
|
import { useFileUpload } from "./core/use-file-upload.js";
|
|
2
2
|
import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
|
-
import { checkAccess, defineVisibility, isAuthorized } from "./
|
|
4
|
-
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./
|
|
3
|
+
import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
|
|
4
|
+
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
|
|
5
|
+
import { definePage, pageHead } from "./route/page.js";
|
|
5
6
|
import { toReturnPath } from "./auth/returnPath.js";
|
|
6
7
|
import { FormattedDate, FormattingScope, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
7
8
|
import { SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
@@ -11,6 +12,6 @@ import { fieldShape, formatDuration, formatField, formatValue, registerSoftrFiel
|
|
|
11
12
|
import { useOverrides } from "./core/use-overrides.js";
|
|
12
13
|
import { useUserTimezone } from "./timezone.js";
|
|
13
14
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
14
|
-
import { Audience } from "./
|
|
15
|
+
import { Audience } from "./route/audience.js";
|
|
15
16
|
|
|
16
|
-
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
17
|
+
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, 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,7 +1,7 @@
|
|
|
1
1
|
import { checkAccess } from "./visibility.js";
|
|
2
2
|
import { useCurrentUser } from "../auth/isomorphic.js";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/route/audience.tsx
|
|
5
5
|
/**
|
|
6
6
|
* Renders `children` to the visitors a rule admits, and `fallback` to the rest.
|
|
7
7
|
*
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { VisibilityRule } from "./visibility.js";
|
|
2
|
+
import { AnyRouteMatch } from "@tanstack/react-router";
|
|
3
|
+
|
|
4
|
+
//#region src/route/page.d.ts
|
|
5
|
+
/** The part of TanStack's `head` context a page setting can read. */
|
|
6
|
+
type HeadContext = {
|
|
7
|
+
match: {
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
matches: ReadonlyArray<{
|
|
11
|
+
id: string;
|
|
12
|
+
}>;
|
|
13
|
+
loaderData?: AnyRouteMatch["loaderData"];
|
|
14
|
+
};
|
|
15
|
+
type HeadSetting<T> = T | ((ctx: HeadContext) => T | undefined);
|
|
16
|
+
type PageSettings = {
|
|
17
|
+
visibility?: VisibilityRule;
|
|
18
|
+
title?: HeadSetting<string>;
|
|
19
|
+
description?: HeadSetting<string>;
|
|
20
|
+
searchEngineIndexing?: HeadSetting<boolean>;
|
|
21
|
+
canonicalUrl?: HeadSetting<string>;
|
|
22
|
+
socialTitle?: HeadSetting<string>;
|
|
23
|
+
socialDescription?: HeadSetting<string>;
|
|
24
|
+
socialImage?: HeadSetting<string>;
|
|
25
|
+
};
|
|
26
|
+
declare const pageBrand: unique symbol;
|
|
27
|
+
/** The settings {@link definePage} has produced. */
|
|
28
|
+
type Page<S extends PageSettings = PageSettings> = S & {
|
|
29
|
+
readonly [pageBrand]: "page";
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Declares a route's settings: who may open it, its title and its SEO tags.
|
|
33
|
+
*
|
|
34
|
+
* One call per route file, at module scope, with an object literal. Studio
|
|
35
|
+
* reads and rewrites each setting written out as a literal; a computed one is
|
|
36
|
+
* changed in the code. A head setting that depends on the route's data is a
|
|
37
|
+
* function of its head context. The route's `beforeLoad` and `head` apply it.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const pageSettings = definePage({
|
|
41
|
+
* visibility: { userGroupTypes: ["LOGGED_IN_USERS"] },
|
|
42
|
+
* title: ({ loaderData }) => loaderData?.name,
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* export const Route = createFileRoute("/_app/projects/$projectId")({
|
|
46
|
+
* beforeLoad: guardRoute(pageSettings),
|
|
47
|
+
* loader: ({ params }) => loadProject({ data: params.projectId }),
|
|
48
|
+
* head: pageHead(pageSettings),
|
|
49
|
+
* component: Project,
|
|
50
|
+
* })
|
|
51
|
+
*/
|
|
52
|
+
declare function definePage<const S extends PageSettings>(settings: S & { [K in Exclude<keyof S, keyof PageSettings>]: never }): Page<S>;
|
|
53
|
+
type Meta = NonNullable<AnyRouteMatch["meta"]>;
|
|
54
|
+
type Links = NonNullable<AnyRouteMatch["links"]>;
|
|
55
|
+
/**
|
|
56
|
+
* The router keeps the deepest route's tag for each title, name and property,
|
|
57
|
+
* so a layout's settings reach every page under it that does not state its own.
|
|
58
|
+
* A canonical URL names one page, so only the page the visitor is on renders
|
|
59
|
+
* its own.
|
|
60
|
+
*
|
|
61
|
+
* Returned rather than called with the route's context: a `head` written as
|
|
62
|
+
* `(ctx) => …` before `loader` stops TypeScript inferring the loader's types.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* head: pageHead(pageSettings)
|
|
66
|
+
*/
|
|
67
|
+
declare function pageHead(page: Page): (ctx: HeadContext) => {
|
|
68
|
+
meta: Meta;
|
|
69
|
+
links: Links;
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { Page, PageSettings, definePage, pageHead };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/route/page.ts
|
|
2
|
+
/**
|
|
3
|
+
* Declares a route's settings: who may open it, its title and its SEO tags.
|
|
4
|
+
*
|
|
5
|
+
* One call per route file, at module scope, with an object literal. Studio
|
|
6
|
+
* reads and rewrites each setting written out as a literal; a computed one is
|
|
7
|
+
* changed in the code. A head setting that depends on the route's data is a
|
|
8
|
+
* function of its head context. The route's `beforeLoad` and `head` apply it.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const pageSettings = definePage({
|
|
12
|
+
* visibility: { userGroupTypes: ["LOGGED_IN_USERS"] },
|
|
13
|
+
* title: ({ loaderData }) => loaderData?.name,
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* export const Route = createFileRoute("/_app/projects/$projectId")({
|
|
17
|
+
* beforeLoad: guardRoute(pageSettings),
|
|
18
|
+
* loader: ({ params }) => loadProject({ data: params.projectId }),
|
|
19
|
+
* head: pageHead(pageSettings),
|
|
20
|
+
* component: Project,
|
|
21
|
+
* })
|
|
22
|
+
*/
|
|
23
|
+
function definePage(settings) {
|
|
24
|
+
return settings;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The router keeps the deepest route's tag for each title, name and property,
|
|
28
|
+
* so a layout's settings reach every page under it that does not state its own.
|
|
29
|
+
* A canonical URL names one page, so only the page the visitor is on renders
|
|
30
|
+
* its own.
|
|
31
|
+
*
|
|
32
|
+
* Returned rather than called with the route's context: a `head` written as
|
|
33
|
+
* `(ctx) => …` before `loader` stops TypeScript inferring the loader's types.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* head: pageHead(pageSettings)
|
|
37
|
+
*/
|
|
38
|
+
function pageHead(page) {
|
|
39
|
+
return (ctx) => {
|
|
40
|
+
const resolve = (setting) => typeof setting === "function" ? setting(ctx) : setting;
|
|
41
|
+
const tag = (setting, make) => {
|
|
42
|
+
const value = resolve(setting);
|
|
43
|
+
return typeof value === "string" && value !== "" ? [make(value)] : [];
|
|
44
|
+
};
|
|
45
|
+
const indexing = resolve(page.searchEngineIndexing);
|
|
46
|
+
return {
|
|
47
|
+
meta: [
|
|
48
|
+
...tag(page.title, (title) => ({ title })),
|
|
49
|
+
...tag(page.description, (content) => ({
|
|
50
|
+
name: "description",
|
|
51
|
+
content
|
|
52
|
+
})),
|
|
53
|
+
...tag(page.socialTitle, (content) => ({
|
|
54
|
+
property: "og:title",
|
|
55
|
+
content
|
|
56
|
+
})),
|
|
57
|
+
...tag(page.socialDescription, (content) => ({
|
|
58
|
+
property: "og:description",
|
|
59
|
+
content
|
|
60
|
+
})),
|
|
61
|
+
...tag(page.socialImage, (content) => ({
|
|
62
|
+
property: "og:image",
|
|
63
|
+
content
|
|
64
|
+
})),
|
|
65
|
+
...typeof indexing === "boolean" ? [{
|
|
66
|
+
name: "robots",
|
|
67
|
+
content: indexing ? "index" : "noindex"
|
|
68
|
+
}] : []
|
|
69
|
+
],
|
|
70
|
+
links: ctx.matches.at(-1)?.id === ctx.match.id ? tag(page.canonicalUrl, (href) => ({
|
|
71
|
+
rel: "canonical",
|
|
72
|
+
href
|
|
73
|
+
})) : []
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
export { definePage, pageHead };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AccessContext, AccessReason, Narrowed,
|
|
1
|
+
import { AccessContext, AccessReason, Narrowed, VisibilityRule } from "./visibility.js";
|
|
2
|
+
import { Page, PageSettings } from "./page.js";
|
|
2
3
|
|
|
3
|
-
//#region src/
|
|
4
|
+
//#region src/route/route.d.ts
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* What a guard does when it denies a visitor: redirect to a path, or throw an
|
|
@@ -42,8 +43,11 @@ declare function isUnauthenticatedError(error: unknown): error is AccessError;
|
|
|
42
43
|
declare function isForbiddenError(error: unknown): error is AccessError;
|
|
43
44
|
/** True for an access error thrown because a signed-out-only route saw a user. */
|
|
44
45
|
declare function isAlreadyAuthenticatedError(error: unknown): error is AccessError;
|
|
46
|
+
type GuardedPage = Page<PageSettings & {
|
|
47
|
+
visibility: VisibilityRule;
|
|
48
|
+
}>;
|
|
45
49
|
/**
|
|
46
|
-
* A `beforeLoad` handler that denies a visitor
|
|
50
|
+
* A `beforeLoad` handler that denies a visitor the page's rule does not admit.
|
|
47
51
|
*
|
|
48
52
|
* Each reason falls back to the app's configured behaviour. Pass `overrides` to
|
|
49
53
|
* change what any one reason does. A redirected signed-out visitor carries the
|
|
@@ -52,9 +56,9 @@ declare function isAlreadyAuthenticatedError(error: unknown): error is AccessErr
|
|
|
52
56
|
* Throws a router redirect, so it belongs in `beforeLoad` and nowhere else.
|
|
53
57
|
*
|
|
54
58
|
* @example
|
|
55
|
-
* beforeLoad: guardRoute(
|
|
59
|
+
* beforeLoad: guardRoute(pageSettings, { unauthenticated: "/sign-in" })
|
|
56
60
|
*/
|
|
57
|
-
declare function guardRoute(
|
|
61
|
+
declare function guardRoute(page: GuardedPage, overrides?: GuardOverrides): (opts: {
|
|
58
62
|
context: AccessContext;
|
|
59
63
|
location: {
|
|
60
64
|
href: string;
|
|
@@ -66,7 +70,7 @@ declare function guardRoute(visibility: Visibility, overrides?: GuardOverrides):
|
|
|
66
70
|
*
|
|
67
71
|
* @example
|
|
68
72
|
* beforeLoad: (opts) => {
|
|
69
|
-
* assertRouteAccess(
|
|
73
|
+
* assertRouteAccess(pageSettings, opts)
|
|
70
74
|
* if (opts.params.projectId === "demo") throw redirect({ to: "/projects" })
|
|
71
75
|
* }
|
|
72
76
|
*/
|
|
@@ -75,7 +79,9 @@ declare function assertRouteAccess<R extends VisibilityRule, O extends {
|
|
|
75
79
|
location: {
|
|
76
80
|
href: string;
|
|
77
81
|
};
|
|
78
|
-
}>(
|
|
82
|
+
}>(page: Page<PageSettings & {
|
|
83
|
+
visibility: R;
|
|
84
|
+
}>, opts: O): asserts opts is O & {
|
|
79
85
|
context: Narrowed<R>;
|
|
80
86
|
};
|
|
81
87
|
//#endregion
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { panic } from "../assertions.js";
|
|
2
|
+
import { ACCESS_REASONS, checkRule } from "./visibility.js";
|
|
2
3
|
import { softrConfig } from "../softr-config.js";
|
|
3
4
|
import { redirect } from "@tanstack/react-router";
|
|
4
5
|
|
|
5
|
-
//#region src/
|
|
6
|
+
//#region src/route/route.ts
|
|
6
7
|
function isAccessReason(value) {
|
|
7
8
|
return ACCESS_REASONS.some((reason) => reason === value);
|
|
8
9
|
}
|
|
@@ -52,8 +53,11 @@ function thrownFor(reason, overrides, location) {
|
|
|
52
53
|
reason
|
|
53
54
|
};
|
|
54
55
|
}
|
|
56
|
+
function ruleOf(page) {
|
|
57
|
+
return page.visibility ?? panic("the page states no visibility rule to guard it with");
|
|
58
|
+
}
|
|
55
59
|
/**
|
|
56
|
-
* A `beforeLoad` handler that denies a visitor
|
|
60
|
+
* A `beforeLoad` handler that denies a visitor the page's rule does not admit.
|
|
57
61
|
*
|
|
58
62
|
* Each reason falls back to the app's configured behaviour. Pass `overrides` to
|
|
59
63
|
* change what any one reason does. A redirected signed-out visitor carries the
|
|
@@ -62,11 +66,11 @@ function thrownFor(reason, overrides, location) {
|
|
|
62
66
|
* Throws a router redirect, so it belongs in `beforeLoad` and nowhere else.
|
|
63
67
|
*
|
|
64
68
|
* @example
|
|
65
|
-
* beforeLoad: guardRoute(
|
|
69
|
+
* beforeLoad: guardRoute(pageSettings, { unauthenticated: "/sign-in" })
|
|
66
70
|
*/
|
|
67
|
-
function guardRoute(
|
|
71
|
+
function guardRoute(page, overrides) {
|
|
68
72
|
return ({ context, location }) => {
|
|
69
|
-
const result =
|
|
73
|
+
const result = checkRule(ruleOf(page), context);
|
|
70
74
|
if (result.ok) return;
|
|
71
75
|
throw thrownFor(result.reason, overrides, location);
|
|
72
76
|
};
|
|
@@ -77,12 +81,12 @@ function guardRoute(visibility, overrides) {
|
|
|
77
81
|
*
|
|
78
82
|
* @example
|
|
79
83
|
* beforeLoad: (opts) => {
|
|
80
|
-
* assertRouteAccess(
|
|
84
|
+
* assertRouteAccess(pageSettings, opts)
|
|
81
85
|
* if (opts.params.projectId === "demo") throw redirect({ to: "/projects" })
|
|
82
86
|
* }
|
|
83
87
|
*/
|
|
84
|
-
function assertRouteAccess(
|
|
85
|
-
const result =
|
|
88
|
+
function assertRouteAccess(page, opts) {
|
|
89
|
+
const result = checkRule(ruleOf(page), opts.context);
|
|
86
90
|
if (result.ok) return;
|
|
87
91
|
throw thrownFor(result.reason, void 0, opts.location);
|
|
88
92
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SoftrUserGroupId, User } from "../auth/user.js";
|
|
2
2
|
import { SoftrContext } from "../auth/context.js";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/route/visibility.d.ts
|
|
5
5
|
type AudienceSet = readonly ["NON_LOGGED_IN_USERS"] | readonly ["LOGGED_IN_USERS"] | readonly ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] | readonly ["LOGGED_IN_USERS", "NON_LOGGED_IN_USERS"];
|
|
6
6
|
/**
|
|
7
7
|
* Who may see a route or region. `customGroups` narrows only the
|
|
@@ -48,18 +48,13 @@ type Narrowed<R extends VisibilityRule> = R["userGroupTypes"] extends ReadonlyAr
|
|
|
48
48
|
user: null;
|
|
49
49
|
} : AccessContext : AccessContext;
|
|
50
50
|
/**
|
|
51
|
-
* Declares who may see a
|
|
52
|
-
*
|
|
53
|
-
* At module scope in a route file, with a literal rule: the build reads these
|
|
54
|
-
* declarations out of the source to publish the app's route visibility.
|
|
51
|
+
* Declares who may see a region of the page. A route's own rule is the
|
|
52
|
+
* `visibility` of its `definePage`.
|
|
55
53
|
*
|
|
56
54
|
* @example
|
|
57
|
-
* const
|
|
55
|
+
* const staffOnly = defineVisibility({ userGroupTypes: ["LOGGED_IN_USERS"], customGroups: ["…"] })
|
|
58
56
|
*
|
|
59
|
-
*
|
|
60
|
-
* beforeLoad: guardRoute(visibility),
|
|
61
|
-
* component: About,
|
|
62
|
-
* })
|
|
57
|
+
* <Audience visibility={staffOnly} fallback={null}>…</Audience>
|
|
63
58
|
*/
|
|
64
59
|
declare function defineVisibility<const R extends VisibilityRule>(rule: R): Visibility<R>;
|
|
65
60
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/
|
|
1
|
+
//#region src/route/visibility.ts
|
|
2
2
|
/**
|
|
3
3
|
* Why a visitor was denied.
|
|
4
4
|
*
|
|
@@ -13,8 +13,12 @@ const ACCESS_REASONS = [
|
|
|
13
13
|
];
|
|
14
14
|
/** The decision and, when a visitor is denied, the reason. Throws nothing. */
|
|
15
15
|
function checkAccess(visibility, context) {
|
|
16
|
+
return checkRule(visibility, context);
|
|
17
|
+
}
|
|
18
|
+
/** {@link checkAccess} for a page's rule, which {@link defineVisibility} has not produced. */
|
|
19
|
+
function checkRule(rule, context) {
|
|
16
20
|
const { user } = context;
|
|
17
|
-
const audiences =
|
|
21
|
+
const audiences = rule.userGroupTypes;
|
|
18
22
|
if (user === null) return audiences.includes("NON_LOGGED_IN_USERS") ? { ok: true } : {
|
|
19
23
|
ok: false,
|
|
20
24
|
reason: "unauthenticated"
|
|
@@ -23,7 +27,7 @@ function checkAccess(visibility, context) {
|
|
|
23
27
|
ok: false,
|
|
24
28
|
reason: "alreadyAuthenticated"
|
|
25
29
|
};
|
|
26
|
-
const required =
|
|
30
|
+
const required = rule.customGroups ?? [];
|
|
27
31
|
if (required.length === 0) return { ok: true };
|
|
28
32
|
return required.some((id) => user.userGroupIds.includes(id)) ? { ok: true } : {
|
|
29
33
|
ok: false,
|
|
@@ -31,18 +35,13 @@ function checkAccess(visibility, context) {
|
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
|
-
* Declares who may see a
|
|
35
|
-
*
|
|
36
|
-
* At module scope in a route file, with a literal rule: the build reads these
|
|
37
|
-
* declarations out of the source to publish the app's route visibility.
|
|
38
|
+
* Declares who may see a region of the page. A route's own rule is the
|
|
39
|
+
* `visibility` of its `definePage`.
|
|
38
40
|
*
|
|
39
41
|
* @example
|
|
40
|
-
* const
|
|
42
|
+
* const staffOnly = defineVisibility({ userGroupTypes: ["LOGGED_IN_USERS"], customGroups: ["…"] })
|
|
41
43
|
*
|
|
42
|
-
*
|
|
43
|
-
* beforeLoad: guardRoute(visibility),
|
|
44
|
-
* component: About,
|
|
45
|
-
* })
|
|
44
|
+
* <Audience visibility={staffOnly} fallback={null}>…</Audience>
|
|
46
45
|
*/
|
|
47
46
|
function defineVisibility(rule) {
|
|
48
47
|
return rule;
|
|
@@ -56,4 +55,4 @@ function isAuthorized(visibility, context) {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
//#endregion
|
|
59
|
-
export { ACCESS_REASONS, checkAccess, defineVisibility, isAuthorized };
|
|
58
|
+
export { ACCESS_REASONS, checkAccess, checkRule, defineVisibility, isAuthorized };
|
package/package.json
CHANGED
|
@@ -7,19 +7,18 @@ description: >-
|
|
|
7
7
|
sign-in (verifyAuthenticator, startAuthenticatorSetup,
|
|
8
8
|
confirmAuthenticatorSetup, skipAuthenticatorSetup) and the ones that change an
|
|
9
9
|
authenticator from a settings page (sendAuthenticatorResetCode,
|
|
10
|
-
resetAuthenticator, removeAuthenticator), the
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
and
|
|
14
|
-
with and its
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
signing in.
|
|
10
|
+
resetAuthenticator, removeAuthenticator), updateProfile for the visitor's own
|
|
11
|
+
name and photo, the emailed password links (requestPasswordReset,
|
|
12
|
+
checkPasswordLink, setPasswordWithLink), and getAuthSettings,
|
|
13
|
+
AuthSettingsProvider and useAuthSettings for which sign-in and sign-up methods
|
|
14
|
+
the app has enabled. Covers the Result shape they answer with and its error
|
|
15
|
+
codes (invalidCredentials, emailTaken, notAuthenticated, notAnImage and the
|
|
16
|
+
rest), the two-step sign-up, why a successful sign-in does not always mean the
|
|
17
|
+
visitor is signed in, and toReturnPath for where to land after signing in.
|
|
19
18
|
metadata:
|
|
20
19
|
type: sub-skill
|
|
21
20
|
library: softr-sdk
|
|
22
|
-
library_version: 0.0.
|
|
21
|
+
library_version: 0.0.71
|
|
23
22
|
---
|
|
24
23
|
|
|
25
24
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -82,6 +81,12 @@ Changing an authenticator from a settings page:
|
|
|
82
81
|
| `auth.resetAuthenticator({ code })` | Unpairs the app, so a new one can be enrolled |
|
|
83
82
|
| `auth.removeAuthenticator({ code })` | Turns the authenticator off |
|
|
84
83
|
|
|
84
|
+
Changing the visitor's own profile:
|
|
85
|
+
|
|
86
|
+
| Call | Does |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `auth.updateProfile({ fullName?, avatar? })` | Changes their name, their photo, or both. `avatar: null` removes the photo |
|
|
89
|
+
|
|
85
90
|
## Setup
|
|
86
91
|
|
|
87
92
|
`auth` is server-only, so every call sits inside a `createServerFn` handler:
|
|
@@ -451,6 +456,20 @@ All three need a signed-in visitor and answer `notAuthenticated` when there is n
|
|
|
451
456
|
|
|
452
457
|
After a reset, enrol the new app with `startAuthenticatorSetup` and `confirmAuthenticatorSetup`, which is also where the new backup codes come from. After a removal, an app that requires an authenticator asks the visitor to set one up again on their next sign-in.
|
|
453
458
|
|
|
459
|
+
### Changing the visitor's name and photo
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
const result = await auth.updateProfile({ fullName: 'Ann Lee' })
|
|
463
|
+
const result = await auth.updateProfile({ avatar: file }) // a File from an <input type="file">
|
|
464
|
+
const result = await auth.updateProfile({ avatar: null }) // removes the photo
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
It answers with the updated `User`. Pass the file through a server function that takes `FormData`. The photo must be a PNG, JPEG or GIF of at most 5 MB, or it answers `notAnImage` or `fileTooLarge`. A blank or unacceptable name answers `invalidName`.
|
|
468
|
+
|
|
469
|
+
Only some apps can store a photo. Show the photo controls only when `user.avatarEditable` is true; elsewhere a photo answers `avatarNotSupported`.
|
|
470
|
+
|
|
471
|
+
The page keeps showing the old name and photo until its route context loads again, so call `router.invalidate()` after a success.
|
|
472
|
+
|
|
454
473
|
## References
|
|
455
474
|
|
|
456
475
|
- [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor these calls establish
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.71
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**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.
|
|
12
|
+
library_version: 0.0.71
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
# The current user
|
|
@@ -31,6 +31,8 @@ type User = {
|
|
|
31
31
|
email: string
|
|
32
32
|
fullName: string
|
|
33
33
|
userGroupIds: Array<SoftrUserGroupId> // the app's own group ids
|
|
34
|
+
avatarUrl: string | null // their photo; show their initials when null
|
|
35
|
+
avatarEditable: boolean // whether auth.updateProfile can change the photo
|
|
34
36
|
}
|
|
35
37
|
```
|
|
36
38
|
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.71
|
|
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.
|
|
13
|
+
library_version: 0.0.71
|
|
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.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.71
|
|
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.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.71
|
|
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.
|
|
16
|
+
library_version: 0.0.71
|
|
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. 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.
|
|
12
|
+
library_version: 0.0.71
|
|
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.
|