@softr/sdk 0.0.63-alpha.0 → 0.0.64-alpha.0
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 +1 -31
- package/dist/action.js +1 -26
- package/dist/auth/isomorphic.d.ts +60 -1
- package/dist/auth/isomorphic.js +63 -5
- package/dist/auth/methods/passwordLink.d.ts +47 -0
- package/dist/auth/methods/passwordLink.js +86 -0
- package/dist/auth/methods/settings.d.ts +11 -0
- package/dist/auth/methods/settings.js +17 -0
- package/dist/auth/methods/signup.js +1 -0
- package/dist/auth/methods/social.js +1 -0
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/platform.d.ts +46 -0
- package/dist/auth/platform.js +54 -0
- package/dist/auth/server.d.ts +6 -0
- package/dist/auth/server.js +7 -1
- package/dist/client.d.ts +1 -2
- package/dist/client.js +1 -2
- package/dist/core/file-upload.d.ts +2 -7
- package/dist/core/file-upload.js +1 -6
- package/dist/core/use-file-upload.d.ts +2 -2
- package/dist/datasource/record.js +11 -5
- package/dist/default-error-overlay.d.ts +2 -2
- package/dist/formatting.d.ts +3 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/mcp.d.ts +2 -8
- package/dist/mcp.js +8 -8
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/dist/server.d.ts +1 -2
- package/dist/server.js +1 -2
- package/dist/softr-config.d.ts +1 -0
- package/dist/softr-config.js +26 -5
- package/dist/visibility/route.d.ts +26 -6
- package/dist/visibility/route.js +36 -13
- package/package.json +33 -12
- package/schema/softr.schema.json +42 -1
- package/skills/softr-sdk/SKILL.md +7 -10
- package/skills/softr-sdk/auth/SKILL.md +129 -31
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +4 -5
- package/skills/softr-sdk/formatting/SKILL.md +4 -4
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +7 -14
- package/skills/softr-sdk/pwa/SKILL.md +3 -3
- 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 +24 -12
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
- package/dist/comments/comments.d.ts +0 -168
- package/dist/comments/comments.js +0 -413
- package/dist/comments/use-comment-composer.d.ts +0 -46
- package/dist/comments/use-comment-composer.js +0 -134
- package/skills/softr-sdk/comments/SKILL.md +0 -181
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DatasourceAction } from "../action.js";
|
|
2
2
|
import { FileAttachment } from "./file-upload.js";
|
|
3
3
|
|
|
4
4
|
//#region src/core/use-file-upload.d.ts
|
|
@@ -16,7 +16,7 @@ type FileItem = {
|
|
|
16
16
|
filename: string;
|
|
17
17
|
error: Error;
|
|
18
18
|
};
|
|
19
|
-
declare function useFileUpload(action: DatasourceAction
|
|
19
|
+
declare function useFileUpload(action: DatasourceAction, options?: {
|
|
20
20
|
initialAttachments?: FileAttachment[];
|
|
21
21
|
}): {
|
|
22
22
|
files: FileItem[];
|
|
@@ -5,7 +5,7 @@ import { readSessionToken } from "../auth/session.js";
|
|
|
5
5
|
import { isDatasourceAction } from "../action.js";
|
|
6
6
|
import { buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy } from "./filter.js";
|
|
7
7
|
import { andAll, makeFieldSet, makeSortSet } from "./fields.js";
|
|
8
|
-
import { getRequestHeader } from "@tanstack/react-start/server";
|
|
8
|
+
import { getRequestHeader, getRequestUrl } from "@tanstack/react-start/server";
|
|
9
9
|
|
|
10
10
|
//#region src/datasource/record.ts
|
|
11
11
|
const MAX_RECORD_COUNT = 100;
|
|
@@ -17,13 +17,19 @@ const dsUrl = (actionId) => {
|
|
|
17
17
|
return `${withoutTrailingSlash(datasourceApiUrl)}/v1/datasource/vibe/applications/${applicationId}/actions/${actionId}`;
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
-
* The datasource API wants to know which page made the call.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* The datasource API wants to know which page made the call, and rejects a call without it.
|
|
21
|
+
*
|
|
22
|
+
* A call from the browser has a Referer on this app's origin, and that Referer names the page.
|
|
23
|
+
* A page load has the previous page as its Referer: Studio, or none for a pasted link.
|
|
24
|
+
* There the page is the request itself, so we send the request URL.
|
|
23
25
|
*/
|
|
24
26
|
function refererHeader() {
|
|
27
|
+
const page = getRequestUrl({
|
|
28
|
+
xForwardedHost: true,
|
|
29
|
+
xForwardedProto: true
|
|
30
|
+
});
|
|
25
31
|
const referer = getRequestHeader("referer");
|
|
26
|
-
return
|
|
32
|
+
return { Referer: referer !== void 0 && URL.parse(referer)?.origin === page.origin ? referer : page.href };
|
|
27
33
|
}
|
|
28
34
|
async function describeFailure(res) {
|
|
29
35
|
const body = await res.json().catch(() => null);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react6 from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/default-error-overlay.d.ts
|
|
4
4
|
type Props = {
|
|
@@ -8,6 +8,6 @@ type Props = {
|
|
|
8
8
|
declare function DefaultErrorOverlay({
|
|
9
9
|
error,
|
|
10
10
|
reset
|
|
11
|
-
}: Props):
|
|
11
|
+
}: Props): react6.JSX.Element;
|
|
12
12
|
//#endregion
|
|
13
13
|
export { DefaultErrorOverlay };
|
package/dist/formatting.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react0 from "react";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
@@ -94,7 +94,7 @@ declare function FormattingScope({
|
|
|
94
94
|
/** A BCP 47 locale, such as `de-DE`. It drops the app's fixed date format inside the scope. */
|
|
95
95
|
locale?: string;
|
|
96
96
|
children?: ReactNode;
|
|
97
|
-
}):
|
|
97
|
+
}): react0.FunctionComponentElement<react0.ProviderProps<Scope>>;
|
|
98
98
|
/**
|
|
99
99
|
* A date in the app's format, safe to render on the server. A timestamp whose
|
|
100
100
|
* zone is not known yet renders `fallback` until the browser supplies it, so
|
|
@@ -115,6 +115,6 @@ declare function FormattedDate({
|
|
|
115
115
|
time?: boolean;
|
|
116
116
|
/** What renders while the visitor's zone is unknown. */
|
|
117
117
|
fallback?: ReactNode;
|
|
118
|
-
}):
|
|
118
|
+
}): react0.FunctionComponentElement<react0.FragmentProps>;
|
|
119
119
|
//#endregion
|
|
120
120
|
export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
2
|
-
import { ActionPermissions, SoftrDatasources,
|
|
2
|
+
import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { FileAttachment } from "./core/file-upload.js";
|
|
4
4
|
import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
5
|
-
import { CommentAttachmentInput, CommentMention, CommentThread, CommentView } from "./comments/comments.js";
|
|
6
5
|
import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
7
6
|
import { SoftrContext } from "./auth/context.js";
|
|
7
|
+
import { AuthSettings } from "./auth/platform.js";
|
|
8
8
|
import { checkAccess, defineVisibility, isAuthorized } from "./visibility/visibility.js";
|
|
9
|
-
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./visibility/route.js";
|
|
10
|
-
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser } from "./auth/isomorphic.js";
|
|
9
|
+
import { assertRouteAccess, authRedirectTarget, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./visibility/route.js";
|
|
10
|
+
import { AuthOptions, AuthSettingsProvider, getAuthSettings, getSoftrContext, initialSoftrContext, softrAuthMiddleware, useAuthSettings, useCurrentUser } from "./auth/isomorphic.js";
|
|
11
11
|
import { softrMcpMiddleware } from "./mcp.js";
|
|
12
12
|
import { useUserTimezone } from "./timezone.js";
|
|
13
13
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
14
14
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
15
15
|
import { Audience } from "./visibility/audience.js";
|
|
16
|
-
export { type ActionPermissions, Audience, type
|
|
16
|
+
export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, authRedirectTarget, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, formatDate, formatDateTime, formatNumber, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, usePwaInstall, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useFileUpload } from "./core/use-file-upload.js";
|
|
2
|
-
import {
|
|
2
|
+
import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { checkAccess, defineVisibility, isAuthorized } from "./visibility/visibility.js";
|
|
4
|
-
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./visibility/route.js";
|
|
4
|
+
import { assertRouteAccess, authRedirectTarget, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./visibility/route.js";
|
|
5
5
|
import { FormattedDate, FormattingScope, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
6
6
|
import { SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
7
|
-
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser } from "./auth/isomorphic.js";
|
|
7
|
+
import { AuthSettingsProvider, getAuthSettings, getSoftrContext, initialSoftrContext, softrAuthMiddleware, useAuthSettings, useCurrentUser } from "./auth/isomorphic.js";
|
|
8
8
|
import { softrMcpMiddleware } from "./mcp.js";
|
|
9
9
|
import { useUserTimezone } from "./timezone.js";
|
|
10
10
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
11
11
|
import { Audience } from "./visibility/audience.js";
|
|
12
12
|
|
|
13
|
-
export { Audience, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess,
|
|
13
|
+
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, authRedirectTarget, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, formatDate, formatDateTime, formatNumber, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, usePwaInstall, useUserTimezone };
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatasourceAction } from "./action.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _tanstack_react_start4 from "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
5
|
|
|
@@ -12,12 +12,6 @@ type SoftrMcpOptions = {
|
|
|
12
12
|
* decide who may run the ones that are listed.
|
|
13
13
|
*/
|
|
14
14
|
actions?: Array<DatasourceAction>;
|
|
15
|
-
/**
|
|
16
|
-
* The app's sign-in page, as a path on this app. A signed-out visitor who
|
|
17
|
-
* opens the approval page is sent here with `?redirect=` pointing back at it.
|
|
18
|
-
* Defaults to `/login`.
|
|
19
|
-
*/
|
|
20
|
-
loginPath?: string;
|
|
21
15
|
};
|
|
22
16
|
/**
|
|
23
17
|
* Serves the MCP endpoint and its OAuth flow from the app's own origin.
|
|
@@ -37,6 +31,6 @@ type SoftrMcpOptions = {
|
|
|
37
31
|
* ],
|
|
38
32
|
* }))
|
|
39
33
|
*/
|
|
40
|
-
declare function softrMcpMiddleware(options?: SoftrMcpOptions):
|
|
34
|
+
declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start4.RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
41
35
|
//#endregion
|
|
42
36
|
export { softrMcpMiddleware };
|
package/dist/mcp.js
CHANGED
|
@@ -4,6 +4,7 @@ import { sessionConfig } from "./auth/config.js";
|
|
|
4
4
|
import { readSessionToken, withSessionToken } from "./auth/session.js";
|
|
5
5
|
import { makePlatform, platformApiUrl } from "./auth/platform.js";
|
|
6
6
|
import { isDatasourceAction } from "./action.js";
|
|
7
|
+
import { softrConfig } from "./visibility/route.js";
|
|
7
8
|
import { ds } from "./datasource/datasource.js";
|
|
8
9
|
import { createMiddleware } from "@tanstack/react-start";
|
|
9
10
|
import { z } from "zod";
|
|
@@ -38,8 +39,7 @@ const SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26"];
|
|
|
38
39
|
* }))
|
|
39
40
|
*/
|
|
40
41
|
function softrMcpMiddleware(options = {}) {
|
|
41
|
-
const
|
|
42
|
-
assert(loginPath.startsWith("/") && new URL(loginPath, "https://app.invalid").origin === "https://app.invalid", `softrMcpMiddleware's loginPath must be a path on this app, got "${loginPath}"`);
|
|
42
|
+
const login = softrConfig().auth.routes.login;
|
|
43
43
|
const actions = /* @__PURE__ */ new Map();
|
|
44
44
|
for (const action of options.actions ?? []) {
|
|
45
45
|
assert(isDatasourceAction(action), "softrMcpMiddleware takes actions built by defineQuery or defineMutation");
|
|
@@ -68,7 +68,7 @@ function softrMcpMiddleware(options = {}) {
|
|
|
68
68
|
client_id_metadata_document_supported: true,
|
|
69
69
|
scopes_supported: SCOPES
|
|
70
70
|
});
|
|
71
|
-
case AUTHORIZE_PATH: return await authorize(request,
|
|
71
|
+
case AUTHORIZE_PATH: return await authorize(request, login);
|
|
72
72
|
case TOKEN_PATH: return await issueToken(request);
|
|
73
73
|
case MCP_PATH: return await rpc(request, origin, actions);
|
|
74
74
|
default: return await next();
|
|
@@ -92,14 +92,14 @@ const authorizeQuerySchema = z.object({
|
|
|
92
92
|
* because this runs on the app's origin. The client and the code are the
|
|
93
93
|
* platform's, so we ask it for both.
|
|
94
94
|
*/
|
|
95
|
-
async function authorize(request,
|
|
95
|
+
async function authorize(request, login) {
|
|
96
96
|
const url = new URL(request.url);
|
|
97
97
|
const query = authorizeQuerySchema.safeParse(Object.fromEntries(url.searchParams));
|
|
98
98
|
if (!query.success) return html(400, page("This request is not valid", "The app that sent you here left something out. Close this window and try connecting again."));
|
|
99
99
|
if (request.method !== "GET" && request.method !== "POST") return methodNotAllowed("GET, POST");
|
|
100
100
|
const sessionToken = readSessionToken();
|
|
101
101
|
const user = sessionToken === null ? null : await resolveUser(sessionToken);
|
|
102
|
-
if (user === null || sessionToken === null) return html(200, signInPage(url,
|
|
102
|
+
if (user === null || sessionToken === null) return html(200, signInPage(url, login));
|
|
103
103
|
const client = await platformConsent(query.data);
|
|
104
104
|
if (!client.ok) return html(400, page("We can't connect this right now", escapeHtml(client.message)));
|
|
105
105
|
if (request.method === "GET") return html(200, consentPage(user, url, client.name));
|
|
@@ -539,9 +539,9 @@ function consentPage(user, url, clientName) {
|
|
|
539
539
|
<button type="submit" name="decision" value="deny">Cancel</button>
|
|
540
540
|
</form>`);
|
|
541
541
|
}
|
|
542
|
-
function signInPage(url,
|
|
543
|
-
const back =
|
|
544
|
-
return page("Sign in first", "You need to be signed in to this app before you can connect an assistant to it.", `<a class="primary" href="${escapeHtml(
|
|
542
|
+
function signInPage(url, login) {
|
|
543
|
+
const back = new URLSearchParams({ [login.redirectParam]: url.pathname + url.search });
|
|
544
|
+
return page("Sign in first", "You need to be signed in to this app before you can connect an assistant to it.", `<a class="primary" href="${escapeHtml(`${login.path}?${back}`)}">Sign in</a>`);
|
|
545
545
|
}
|
|
546
546
|
function page(title, body, actions = "") {
|
|
547
547
|
return `<!doctype html>
|
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react4 from "react";
|
|
2
2
|
import "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/pwa.d.ts
|
|
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
|
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
|
-
declare function SoftrPwaHead():
|
|
26
|
+
declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
|
|
27
27
|
type PwaPlatform = "ios" | "android" | "desktop";
|
|
28
28
|
type PwaInstall = {
|
|
29
29
|
available: false;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { upload } from "./core/file-upload.js";
|
|
2
|
-
import { comments } from "./comments/comments.js";
|
|
3
2
|
import { auth } from "./auth/server.js";
|
|
4
3
|
import { ds } from "./datasource/datasource.js";
|
|
5
4
|
import { workflow } from "./core/workflow.js";
|
|
6
5
|
import { integration } from "./core/integration.js";
|
|
7
6
|
import "@tanstack/react-start/server-only";
|
|
8
|
-
export { auth,
|
|
7
|
+
export { auth, ds, integration, upload, workflow };
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,6 @@ import { ds } from "./datasource/datasource.js";
|
|
|
3
3
|
import { auth } from "./auth/server.js";
|
|
4
4
|
import { workflow } from "./core/workflow.js";
|
|
5
5
|
import { integration } from "./core/integration.js";
|
|
6
|
-
import { comments } from "./comments/comments.js";
|
|
7
6
|
import "@tanstack/react-start/server-only";
|
|
8
7
|
|
|
9
|
-
export { auth,
|
|
8
|
+
export { auth, ds, integration, upload, workflow };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { z } from "zod";
|
package/dist/softr-config.js
CHANGED
|
@@ -9,11 +9,32 @@ const AccessBehaviour = z.union([
|
|
|
9
9
|
}),
|
|
10
10
|
z.object({ strategy: z.literal("throw") })
|
|
11
11
|
], { error: "must be a path like \"/login\", { \"strategy\": \"redirect\", \"to\": \"/login\" } or { \"strategy\": \"throw\" }" });
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const AppPath = z.string().refine((path) => path.startsWith("/") && !path.includes("?") && !path.includes("#") && new URL(path, "https://app.invalid").origin === "https://app.invalid", { error: "must be a path on this app like \"/login\", with no \"?\" or \"#\"" });
|
|
13
|
+
const SoftrConfigSchema = z.object({ auth: z.strictObject({
|
|
14
|
+
routes: z.strictObject({
|
|
15
|
+
login: z.strictObject({
|
|
16
|
+
path: AppPath,
|
|
17
|
+
redirectParam: z.string()
|
|
18
|
+
}),
|
|
19
|
+
resetPassword: z.strictObject({
|
|
20
|
+
path: AppPath,
|
|
21
|
+
tokenParam: z.string()
|
|
22
|
+
}),
|
|
23
|
+
acceptInvitation: z.strictObject({
|
|
24
|
+
path: AppPath,
|
|
25
|
+
tokenParam: z.string()
|
|
26
|
+
}),
|
|
27
|
+
magicLink: z.strictObject({
|
|
28
|
+
path: AppPath,
|
|
29
|
+
tokenParam: z.string()
|
|
30
|
+
})
|
|
31
|
+
}),
|
|
32
|
+
guard: z.strictObject({
|
|
33
|
+
unauthenticated: AccessBehaviour,
|
|
34
|
+
forbidden: AccessBehaviour,
|
|
35
|
+
alreadyAuthenticated: AccessBehaviour
|
|
36
|
+
})
|
|
37
|
+
}) });
|
|
17
38
|
|
|
18
39
|
//#endregion
|
|
19
40
|
export { SoftrConfigSchema };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AccessContext, AccessReason, Narrowed, Visibility, VisibilityRule } from "./visibility.js";
|
|
2
|
+
import "../softr-config.js";
|
|
2
3
|
|
|
3
4
|
//#region src/visibility/route.d.ts
|
|
4
5
|
|
|
@@ -46,7 +47,8 @@ declare function isAlreadyAuthenticatedError(error: unknown): error is AccessErr
|
|
|
46
47
|
* A `beforeLoad` handler that denies a visitor who fails the rule.
|
|
47
48
|
*
|
|
48
49
|
* Each reason falls back to the app's configured behaviour. Pass `overrides` to
|
|
49
|
-
* change what any one reason does.
|
|
50
|
+
* change what any one reason does. A redirected signed-out visitor carries the
|
|
51
|
+
* page they wanted, for {@link authRedirectTarget} to read back.
|
|
50
52
|
*
|
|
51
53
|
* Throws a router redirect, so it belongs in `beforeLoad` and nowhere else.
|
|
52
54
|
*
|
|
@@ -55,17 +57,35 @@ declare function isAlreadyAuthenticatedError(error: unknown): error is AccessErr
|
|
|
55
57
|
*/
|
|
56
58
|
declare function guardRoute(visibility: Visibility, overrides?: GuardOverrides): (opts: {
|
|
57
59
|
context: AccessContext;
|
|
60
|
+
location: {
|
|
61
|
+
href: string;
|
|
62
|
+
};
|
|
58
63
|
}) => void;
|
|
59
64
|
/**
|
|
60
65
|
* Throws what {@link guardRoute} would, for a `beforeLoad` that runs its own
|
|
61
66
|
* checks after the rule. It takes no overrides.
|
|
62
67
|
*
|
|
63
68
|
* @example
|
|
64
|
-
* beforeLoad: (
|
|
65
|
-
* assertRouteAccess(visibility,
|
|
66
|
-
* if (params.projectId === "demo") throw redirect({ to: "/projects" })
|
|
69
|
+
* beforeLoad: (opts) => {
|
|
70
|
+
* assertRouteAccess(visibility, opts)
|
|
71
|
+
* if (opts.params.projectId === "demo") throw redirect({ to: "/projects" })
|
|
67
72
|
* }
|
|
68
73
|
*/
|
|
69
|
-
declare function assertRouteAccess<R extends VisibilityRule
|
|
74
|
+
declare function assertRouteAccess<R extends VisibilityRule, O extends {
|
|
75
|
+
context: AccessContext;
|
|
76
|
+
location: {
|
|
77
|
+
href: string;
|
|
78
|
+
};
|
|
79
|
+
}>(visibility: Visibility<R>, opts: O): asserts opts is O & {
|
|
80
|
+
context: Narrowed<R>;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Where to send a visitor who just signed in: the path a guard sent them away
|
|
84
|
+
* from, or `home`. Anything but a path on this app is `home`.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const target = authRedirectTarget(useLocation({ select: (l) => l.search }), "/")
|
|
88
|
+
*/
|
|
89
|
+
declare function authRedirectTarget(search: Record<string, unknown>, home: string): string;
|
|
70
90
|
//#endregion
|
|
71
|
-
export { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError };
|
|
91
|
+
export { assertRouteAccess, authRedirectTarget, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError };
|
package/dist/visibility/route.js
CHANGED
|
@@ -32,14 +32,24 @@ function isForbiddenError(error) {
|
|
|
32
32
|
function isAlreadyAuthenticatedError(error) {
|
|
33
33
|
return isReason(error, "alreadyAuthenticated");
|
|
34
34
|
}
|
|
35
|
+
function softrConfig() {
|
|
36
|
+
return typeof __SOFTR_CONFIG__ === "undefined" ? panic("softr.json is missing") : __SOFTR_CONFIG__;
|
|
37
|
+
}
|
|
35
38
|
/** What a guard throws for a reason: a redirect, or an {@link AccessError}. */
|
|
36
|
-
function thrownFor(reason, overrides) {
|
|
37
|
-
const behaviour = overrides?.[reason] ?? (
|
|
39
|
+
function thrownFor(reason, overrides, location) {
|
|
40
|
+
const behaviour = overrides?.[reason] ?? softrConfig().auth.guard[reason];
|
|
38
41
|
const resolved = typeof behaviour === "string" ? {
|
|
39
42
|
strategy: "redirect",
|
|
40
43
|
to: behaviour
|
|
41
44
|
} : behaviour;
|
|
42
|
-
if (resolved.strategy === "redirect")
|
|
45
|
+
if (resolved.strategy === "redirect") {
|
|
46
|
+
if (reason !== "unauthenticated") return redirect({ to: resolved.to });
|
|
47
|
+
const { redirectParam } = softrConfig().auth.routes.login;
|
|
48
|
+
return redirect({
|
|
49
|
+
to: resolved.to,
|
|
50
|
+
search: { [redirectParam]: location.href }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
43
53
|
return {
|
|
44
54
|
type: "access_error",
|
|
45
55
|
reason
|
|
@@ -49,7 +59,8 @@ function thrownFor(reason, overrides) {
|
|
|
49
59
|
* A `beforeLoad` handler that denies a visitor who fails the rule.
|
|
50
60
|
*
|
|
51
61
|
* Each reason falls back to the app's configured behaviour. Pass `overrides` to
|
|
52
|
-
* change what any one reason does.
|
|
62
|
+
* change what any one reason does. A redirected signed-out visitor carries the
|
|
63
|
+
* page they wanted, for {@link authRedirectTarget} to read back.
|
|
53
64
|
*
|
|
54
65
|
* Throws a router redirect, so it belongs in `beforeLoad` and nowhere else.
|
|
55
66
|
*
|
|
@@ -57,10 +68,10 @@ function thrownFor(reason, overrides) {
|
|
|
57
68
|
* beforeLoad: guardRoute(visibility, { unauthenticated: "/sign-in" })
|
|
58
69
|
*/
|
|
59
70
|
function guardRoute(visibility, overrides) {
|
|
60
|
-
return ({ context }) => {
|
|
71
|
+
return ({ context, location }) => {
|
|
61
72
|
const result = checkAccess(visibility, context);
|
|
62
73
|
if (result.ok) return;
|
|
63
|
-
throw thrownFor(result.reason, overrides);
|
|
74
|
+
throw thrownFor(result.reason, overrides, location);
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
@@ -68,16 +79,28 @@ function guardRoute(visibility, overrides) {
|
|
|
68
79
|
* checks after the rule. It takes no overrides.
|
|
69
80
|
*
|
|
70
81
|
* @example
|
|
71
|
-
* beforeLoad: (
|
|
72
|
-
* assertRouteAccess(visibility,
|
|
73
|
-
* if (params.projectId === "demo") throw redirect({ to: "/projects" })
|
|
82
|
+
* beforeLoad: (opts) => {
|
|
83
|
+
* assertRouteAccess(visibility, opts)
|
|
84
|
+
* if (opts.params.projectId === "demo") throw redirect({ to: "/projects" })
|
|
74
85
|
* }
|
|
75
86
|
*/
|
|
76
|
-
function assertRouteAccess(visibility,
|
|
77
|
-
const result = checkAccess(visibility, context);
|
|
87
|
+
function assertRouteAccess(visibility, opts) {
|
|
88
|
+
const result = checkAccess(visibility, opts.context);
|
|
78
89
|
if (result.ok) return;
|
|
79
|
-
throw thrownFor(result.reason, void 0);
|
|
90
|
+
throw thrownFor(result.reason, void 0, opts.location);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Where to send a visitor who just signed in: the path a guard sent them away
|
|
94
|
+
* from, or `home`. Anything but a path on this app is `home`.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* const target = authRedirectTarget(useLocation({ select: (l) => l.search }), "/")
|
|
98
|
+
*/
|
|
99
|
+
function authRedirectTarget(search, home) {
|
|
100
|
+
const target = search[softrConfig().auth.routes.login.redirectParam];
|
|
101
|
+
if (typeof target !== "string" || !target.startsWith("/")) return home;
|
|
102
|
+
return new URL(target, "https://app.invalid").origin === "https://app.invalid" ? target : home;
|
|
80
103
|
}
|
|
81
104
|
|
|
82
105
|
//#endregion
|
|
83
|
-
export { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError };
|
|
106
|
+
export { assertRouteAccess, authRedirectTarget, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError, softrConfig };
|
package/package.json
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.64-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./client": {
|
|
11
|
+
"types": "./dist/client.d.ts",
|
|
12
|
+
"import": "./dist/client.js"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"types": "./dist/server.d.ts",
|
|
16
|
+
"import": "./dist/server.js"
|
|
17
|
+
},
|
|
18
|
+
"./vite": {
|
|
19
|
+
"types": "./dist/vite.d.ts",
|
|
20
|
+
"import": "./dist/vite.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"softr-sdk": "./dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"schema",
|
|
29
|
+
"skills",
|
|
30
|
+
"!skills/_artifacts"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
14
35
|
"scripts": {
|
|
15
36
|
"build": "tsdown",
|
|
16
37
|
"test": "node --import ./src/test-utils/setup.ts --test",
|
|
17
38
|
"typecheck": "tsc --noEmit"
|
|
18
39
|
},
|
|
19
40
|
"dependencies": {
|
|
20
|
-
"marked": "^18.0.14",
|
|
21
|
-
"xss": "^1.0.15",
|
|
22
41
|
"zod": "^4.4.3"
|
|
23
42
|
},
|
|
24
43
|
"devDependencies": {
|
|
@@ -44,7 +63,9 @@
|
|
|
44
63
|
"optional": true
|
|
45
64
|
}
|
|
46
65
|
},
|
|
47
|
-
"keywords": [
|
|
66
|
+
"keywords": [
|
|
67
|
+
"tanstack-intent"
|
|
68
|
+
],
|
|
48
69
|
"intent": {
|
|
49
70
|
"version": 1,
|
|
50
71
|
"repo": "@softr/sdk",
|
package/schema/softr.schema.json
CHANGED
|
@@ -7,6 +7,30 @@
|
|
|
7
7
|
"type": "object",
|
|
8
8
|
"description": "Authentication. Written in this repo and read by the app's build.",
|
|
9
9
|
"properties": {
|
|
10
|
+
"routes": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"description": "Where the app's sign-in pages live.",
|
|
13
|
+
"properties": {
|
|
14
|
+
"login": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"description": "The sign-in page.",
|
|
17
|
+
"properties": {
|
|
18
|
+
"path": { "$ref": "#/$defs/appPath" },
|
|
19
|
+
"redirectParam": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "The search param that carries where to go after signing in. Every unauthenticated redirect sets it."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": ["path", "redirectParam"],
|
|
25
|
+
"additionalProperties": false
|
|
26
|
+
},
|
|
27
|
+
"resetPassword": { "$ref": "#/$defs/tokenRoute", "description": "The page a password reset email links to." },
|
|
28
|
+
"acceptInvitation": { "$ref": "#/$defs/tokenRoute", "description": "The page an invitation email links to." },
|
|
29
|
+
"magicLink": { "$ref": "#/$defs/tokenRoute", "description": "The page a magic link email links to." }
|
|
30
|
+
},
|
|
31
|
+
"required": ["login", "resetPassword", "acceptInvitation", "magicLink"],
|
|
32
|
+
"additionalProperties": false
|
|
33
|
+
},
|
|
10
34
|
"guard": {
|
|
11
35
|
"type": "object",
|
|
12
36
|
"description": "What a route guard does when it denies a visitor. A route's own overrides win over these.",
|
|
@@ -28,12 +52,29 @@
|
|
|
28
52
|
"additionalProperties": false
|
|
29
53
|
}
|
|
30
54
|
},
|
|
31
|
-
"required": ["guard"],
|
|
55
|
+
"required": ["routes", "guard"],
|
|
32
56
|
"additionalProperties": false
|
|
33
57
|
}
|
|
34
58
|
},
|
|
35
59
|
"required": ["auth"],
|
|
36
60
|
"$defs": {
|
|
61
|
+
"appPath": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"pattern": "^/[^?#]*$",
|
|
64
|
+
"description": "A path on this app, with no search or hash."
|
|
65
|
+
},
|
|
66
|
+
"tokenRoute": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"path": { "$ref": "#/$defs/appPath" },
|
|
70
|
+
"tokenParam": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "The search param that carries the link's token."
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"required": ["path", "tokenParam"],
|
|
76
|
+
"additionalProperties": false
|
|
77
|
+
},
|
|
37
78
|
"accessBehaviour": {
|
|
38
79
|
"oneOf": [
|
|
39
80
|
{ "type": "string", "description": "A path to redirect to." },
|
|
@@ -6,12 +6,13 @@ description: >-
|
|
|
6
6
|
boundary between them, softrAuthMiddleware, getSoftrContext,
|
|
7
7
|
initialSoftrContext, SoftrContext, and which sub-skill to load for route
|
|
8
8
|
visibility, the current user, reading records, charts and metrics, writing
|
|
9
|
-
records, auth, or showing dates and numbers. Load this when unsure which
|
|
10
|
-
touching the wiring; each sub-skill stands alone
|
|
9
|
+
records, auth, or showing dates and numbers. Load this when unsure which
|
|
10
|
+
sub-skill applies, or when touching the wiring; each sub-skill stands alone
|
|
11
|
+
for its own task.
|
|
11
12
|
metadata:
|
|
12
13
|
type: core
|
|
13
14
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.64-alpha.0
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
# Softr SDK core
|
|
@@ -29,9 +30,9 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
29
30
|
|
|
30
31
|
| Import | Runs | Holds |
|
|
31
32
|
| --- | --- | --- |
|
|
32
|
-
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `
|
|
33
|
-
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration
|
|
34
|
-
| `@softr/sdk/client` | Browser only |
|
|
33
|
+
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
34
|
+
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
|
|
35
|
+
| `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
|
|
35
36
|
| `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
|
|
36
37
|
|
|
37
38
|
## Sub-Skills
|
|
@@ -50,7 +51,6 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
50
51
|
| Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
|
|
51
52
|
| Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
|
|
52
53
|
| Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
|
|
53
|
-
| Add comments to a record | [comments/SKILL.md](./comments/SKILL.md) |
|
|
54
54
|
|
|
55
55
|
## Quick Decision Tree
|
|
56
56
|
|
|
@@ -91,9 +91,6 @@ Adding an install button, or making the app installable?
|
|
|
91
91
|
Showing a date, a time or a number on a page?
|
|
92
92
|
load softr-sdk/formatting
|
|
93
93
|
|
|
94
|
-
Letting visitors comment on a record?
|
|
95
|
-
load softr-sdk/comments
|
|
96
|
-
|
|
97
94
|
Writing a server function, loader, or middleware in general?
|
|
98
95
|
not a Softr concern. Use the TanStack Start skills.
|
|
99
96
|
```
|