@softr/sdk 0.0.65-alpha.0 → 0.0.65

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.
Files changed (43) hide show
  1. package/dist/auth/isomorphic.d.ts +60 -1
  2. package/dist/auth/isomorphic.js +67 -7
  3. package/dist/auth/methods/passwordLink.d.ts +47 -0
  4. package/dist/auth/methods/passwordLink.js +86 -0
  5. package/dist/auth/methods/settings.d.ts +11 -0
  6. package/dist/auth/methods/settings.js +17 -0
  7. package/dist/auth/methods/signup.js +1 -0
  8. package/dist/auth/methods/social.js +1 -0
  9. package/dist/auth/methods/utils.d.ts +1 -1
  10. package/dist/auth/platform.d.ts +46 -0
  11. package/dist/auth/platform.js +54 -0
  12. package/dist/auth/returnPath.d.ts +19 -0
  13. package/dist/auth/returnPath.js +37 -0
  14. package/dist/auth/server.d.ts +6 -0
  15. package/dist/auth/server.js +7 -1
  16. package/dist/default-error-overlay.d.ts +2 -2
  17. package/dist/index.d.ts +4 -2
  18. package/dist/index.js +3 -2
  19. package/dist/mcp.d.ts +2 -8
  20. package/dist/mcp.js +8 -8
  21. package/dist/package.js +1 -1
  22. package/dist/pwa.d.ts +2 -2
  23. package/dist/softr-config.js +5 -15
  24. package/dist/softr-config.schema.js +40 -0
  25. package/dist/visibility/route.d.ts +16 -5
  26. package/dist/visibility/route.js +21 -13
  27. package/dist/vite.js +1 -1
  28. package/package.json +10 -33
  29. package/schema/softr.schema.json +42 -1
  30. package/skills/softr-sdk/SKILL.md +4 -3
  31. package/skills/softr-sdk/auth/SKILL.md +131 -31
  32. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  33. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  34. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  35. package/skills/softr-sdk/formatting/SKILL.md +4 -4
  36. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  37. package/skills/softr-sdk/mcp/SKILL.md +7 -14
  38. package/skills/softr-sdk/pwa/SKILL.md +3 -3
  39. package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
  40. package/skills/softr-sdk/record-queries/SKILL.md +2 -3
  41. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  42. package/skills/softr-sdk/routes-and-visibility/SKILL.md +25 -14
  43. package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DatasourceAction } from "./action.js";
2
- import * as _tanstack_react_start3 from "@tanstack/react-start";
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): _tanstack_react_start3.RequestMiddlewareAfterServer<{}, undefined, undefined>;
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 "./softr-config.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 loginPath = options.loginPath ?? "/login";
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, loginPath);
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, loginPath) {
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, loginPath));
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, loginPath) {
543
- const back = encodeURIComponent(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(loginPath)}?redirect=${back}">Sign in</a>`);
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
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.65-alpha.0";
2
+ var version = "0.0.65";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/pwa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as react3 from "react";
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(): react3.FunctionComponentElement<react3.FragmentProps>;
26
+ declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
27
27
  type PwaPlatform = "ios" | "android" | "desktop";
28
28
  type PwaInstall = {
29
29
  available: false;
@@ -1,19 +1,9 @@
1
- import { z } from "zod";
1
+ import { panic } from "./assertions.js";
2
2
 
3
3
  //#region src/softr-config.ts
4
- const AccessBehaviour = z.union([
5
- z.string(),
6
- z.object({
7
- strategy: z.literal("redirect"),
8
- to: z.string()
9
- }),
10
- z.object({ strategy: z.literal("throw") })
11
- ], { error: "must be a path like \"/login\", { \"strategy\": \"redirect\", \"to\": \"/login\" } or { \"strategy\": \"throw\" }" });
12
- const SoftrConfigSchema = z.object({ auth: z.strictObject({ guard: z.strictObject({
13
- unauthenticated: AccessBehaviour,
14
- forbidden: AccessBehaviour,
15
- alreadyAuthenticated: AccessBehaviour
16
- }) }) });
4
+ function softrConfig() {
5
+ return typeof __SOFTR_CONFIG__ === "undefined" ? panic("softr.json is missing") : __SOFTR_CONFIG__;
6
+ }
17
7
 
18
8
  //#endregion
19
- export { SoftrConfigSchema };
9
+ export { softrConfig };
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/softr-config.schema.ts
4
+ const AccessBehaviour = z.union([
5
+ z.string(),
6
+ z.object({
7
+ strategy: z.literal("redirect"),
8
+ to: z.string()
9
+ }),
10
+ z.object({ strategy: z.literal("throw") })
11
+ ], { error: "must be a path like \"/login\", { \"strategy\": \"redirect\", \"to\": \"/login\" } or { \"strategy\": \"throw\" }" });
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
+ }) });
38
+
39
+ //#endregion
40
+ export { SoftrConfigSchema };
@@ -46,7 +46,8 @@ declare function isAlreadyAuthenticatedError(error: unknown): error is AccessErr
46
46
  * A `beforeLoad` handler that denies a visitor who fails the rule.
47
47
  *
48
48
  * Each reason falls back to the app's configured behaviour. Pass `overrides` to
49
- * change what any one reason does.
49
+ * change what any one reason does. A redirected signed-out visitor carries the
50
+ * page they wanted, for `toReturnPath` to read back.
50
51
  *
51
52
  * Throws a router redirect, so it belongs in `beforeLoad` and nowhere else.
52
53
  *
@@ -55,17 +56,27 @@ declare function isAlreadyAuthenticatedError(error: unknown): error is AccessErr
55
56
  */
56
57
  declare function guardRoute(visibility: Visibility, overrides?: GuardOverrides): (opts: {
57
58
  context: AccessContext;
59
+ location: {
60
+ href: string;
61
+ };
58
62
  }) => void;
59
63
  /**
60
64
  * Throws what {@link guardRoute} would, for a `beforeLoad` that runs its own
61
65
  * checks after the rule. It takes no overrides.
62
66
  *
63
67
  * @example
64
- * beforeLoad: ({ context, params }) => {
65
- * assertRouteAccess(visibility, context)
66
- * if (params.projectId === "demo") throw redirect({ to: "/projects" })
68
+ * beforeLoad: (opts) => {
69
+ * assertRouteAccess(visibility, opts)
70
+ * if (opts.params.projectId === "demo") throw redirect({ to: "/projects" })
67
71
  * }
68
72
  */
69
- declare function assertRouteAccess<R extends VisibilityRule>(visibility: Visibility<R>, context: AccessContext): asserts context is Narrowed<R>;
73
+ declare function assertRouteAccess<R extends VisibilityRule, O extends {
74
+ context: AccessContext;
75
+ location: {
76
+ href: string;
77
+ };
78
+ }>(visibility: Visibility<R>, opts: O): asserts opts is O & {
79
+ context: Narrowed<R>;
80
+ };
70
81
  //#endregion
71
82
  export { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError };
@@ -1,5 +1,5 @@
1
- import { panic } from "../assertions.js";
2
1
  import { ACCESS_REASONS, checkAccess } from "./visibility.js";
2
+ import { softrConfig } from "../softr-config.js";
3
3
  import { redirect } from "@tanstack/react-router";
4
4
 
5
5
  //#region src/visibility/route.ts
@@ -33,13 +33,20 @@ function isAlreadyAuthenticatedError(error) {
33
33
  return isReason(error, "alreadyAuthenticated");
34
34
  }
35
35
  /** What a guard throws for a reason: a redirect, or an {@link AccessError}. */
36
- function thrownFor(reason, overrides) {
37
- const behaviour = overrides?.[reason] ?? (typeof __SOFTR_CONFIG__ === "undefined" ? panic("softr.json is missing, so no guard behaviour is configured") : __SOFTR_CONFIG__.auth.guard[reason]);
36
+ function thrownFor(reason, overrides, location) {
37
+ const behaviour = overrides?.[reason] ?? softrConfig().auth.guard[reason];
38
38
  const resolved = typeof behaviour === "string" ? {
39
39
  strategy: "redirect",
40
40
  to: behaviour
41
41
  } : behaviour;
42
- if (resolved.strategy === "redirect") return redirect({ to: resolved.to });
42
+ if (resolved.strategy === "redirect") {
43
+ if (reason !== "unauthenticated") return redirect({ to: resolved.to });
44
+ const { redirectParam } = softrConfig().auth.routes.login;
45
+ return redirect({
46
+ to: resolved.to,
47
+ search: { [redirectParam]: location.href }
48
+ });
49
+ }
43
50
  return {
44
51
  type: "access_error",
45
52
  reason
@@ -49,7 +56,8 @@ function thrownFor(reason, overrides) {
49
56
  * A `beforeLoad` handler that denies a visitor who fails the rule.
50
57
  *
51
58
  * Each reason falls back to the app's configured behaviour. Pass `overrides` to
52
- * change what any one reason does.
59
+ * change what any one reason does. A redirected signed-out visitor carries the
60
+ * page they wanted, for `toReturnPath` to read back.
53
61
  *
54
62
  * Throws a router redirect, so it belongs in `beforeLoad` and nowhere else.
55
63
  *
@@ -57,10 +65,10 @@ function thrownFor(reason, overrides) {
57
65
  * beforeLoad: guardRoute(visibility, { unauthenticated: "/sign-in" })
58
66
  */
59
67
  function guardRoute(visibility, overrides) {
60
- return ({ context }) => {
68
+ return ({ context, location }) => {
61
69
  const result = checkAccess(visibility, context);
62
70
  if (result.ok) return;
63
- throw thrownFor(result.reason, overrides);
71
+ throw thrownFor(result.reason, overrides, location);
64
72
  };
65
73
  }
66
74
  /**
@@ -68,15 +76,15 @@ function guardRoute(visibility, overrides) {
68
76
  * checks after the rule. It takes no overrides.
69
77
  *
70
78
  * @example
71
- * beforeLoad: ({ context, params }) => {
72
- * assertRouteAccess(visibility, context)
73
- * if (params.projectId === "demo") throw redirect({ to: "/projects" })
79
+ * beforeLoad: (opts) => {
80
+ * assertRouteAccess(visibility, opts)
81
+ * if (opts.params.projectId === "demo") throw redirect({ to: "/projects" })
74
82
  * }
75
83
  */
76
- function assertRouteAccess(visibility, context) {
77
- const result = checkAccess(visibility, context);
84
+ function assertRouteAccess(visibility, opts) {
85
+ const result = checkAccess(visibility, opts.context);
78
86
  if (result.ok) return;
79
- throw thrownFor(result.reason, void 0);
87
+ throw thrownFor(result.reason, void 0, opts.location);
80
88
  }
81
89
 
82
90
  //#endregion
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { assert, panic } from "./assertions.js";
2
- import { SoftrConfigSchema } from "./softr-config.js";
2
+ import { SoftrConfigSchema } from "./softr-config.schema.js";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { z } from "zod";
package/package.json CHANGED
@@ -1,37 +1,16 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.65-alpha.0",
3
+ "version": "0.0.65",
4
4
  "type": "module",
5
5
  "exports": {
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
- },
6
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
7
+ "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
8
+ "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
9
+ "./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
10
+ },
11
+ "bin": { "softr-sdk": "./dist/cli.js" },
12
+ "files": ["dist", "schema", "skills", "!skills/_artifacts"],
13
+ "publishConfig": { "access": "public" },
35
14
  "scripts": {
36
15
  "build": "tsdown",
37
16
  "test": "node --import ./src/test-utils/setup.ts --test",
@@ -63,9 +42,7 @@
63
42
  "optional": true
64
43
  }
65
44
  },
66
- "keywords": [
67
- "tanstack-intent"
68
- ],
45
+ "keywords": ["tanstack-intent"],
69
46
  "intent": {
70
47
  "version": 1,
71
48
  "repo": "@softr/sdk",
@@ -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 sub-skill applies, or when
10
- touching the wiring; each sub-skill stands alone for its own task.
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.61
15
+ library_version: 0.0.65
15
16
  ---
16
17
 
17
18
  # Softr SDK core