@softr/sdk 0.0.64-alpha.1 → 0.0.65-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.
Files changed (38) hide show
  1. package/dist/auth/isomorphic.d.ts +4 -63
  2. package/dist/auth/isomorphic.js +5 -63
  3. package/dist/auth/methods/signup.js +0 -1
  4. package/dist/auth/methods/social.js +0 -1
  5. package/dist/auth/methods/utils.d.ts +1 -1
  6. package/dist/auth/platform.js +0 -54
  7. package/dist/auth/server.d.ts +0 -6
  8. package/dist/auth/server.js +1 -7
  9. package/dist/index.d.ts +3 -4
  10. package/dist/index.js +3 -3
  11. package/dist/mcp.d.ts +8 -2
  12. package/dist/mcp.js +8 -8
  13. package/dist/package.js +1 -1
  14. package/dist/softr-config.js +5 -26
  15. package/dist/visibility/route.d.ts +6 -34
  16. package/dist/visibility/route.js +13 -55
  17. package/package.json +1 -1
  18. package/schema/softr.schema.json +1 -42
  19. package/skills/softr-sdk/SKILL.md +3 -4
  20. package/skills/softr-sdk/auth/SKILL.md +31 -131
  21. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  22. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  23. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  24. package/skills/softr-sdk/formatting/SKILL.md +4 -4
  25. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  26. package/skills/softr-sdk/mcp/SKILL.md +14 -7
  27. package/skills/softr-sdk/pwa/SKILL.md +3 -3
  28. package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
  29. package/skills/softr-sdk/record-queries/SKILL.md +3 -2
  30. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  31. package/skills/softr-sdk/routes-and-visibility/SKILL.md +14 -25
  32. package/skills/softr-sdk/workflows/SKILL.md +1 -1
  33. package/dist/auth/methods/passwordLink.d.ts +0 -47
  34. package/dist/auth/methods/passwordLink.js +0 -86
  35. package/dist/auth/methods/settings.d.ts +0 -11
  36. package/dist/auth/methods/settings.js +0 -17
  37. package/dist/auth/platform.d.ts +0 -46
  38. package/dist/softr-config.d.ts +0 -1
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.64-alpha.1
16
+ library_version: 0.0.61
17
17
  ---
18
18
 
19
19
  **Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
@@ -49,6 +49,7 @@ Four constraints on that shape, because Softr reads the file rather than running
49
49
  - The call must initialise a top-level `const`.
50
50
  - The import must be a direct named import from `@softr/sdk`.
51
51
  - The route guards itself in `beforeLoad`, naming a rule declared in the same file. A rule imported from elsewhere is rejected.
52
+ - Every value inside the literal is written there too. A group id taken from a `const`, a spread or an import is rejected, even when two routes share it: repeat the id on each.
52
53
  - Never declare on `__root.tsx`. The root cannot carry an audience, so a declaration there would gate nothing while looking like it does. Softr rejects it. Put the rule on a layout below the root instead.
53
54
 
54
55
  `userGroupTypes` is a set, so a rule names one audience or several:
@@ -68,7 +69,7 @@ defineVisibility({
68
69
  ```
69
70
 
70
71
  Everyone is both audiences, as in the first line above. A set names each audience at most once and
71
- is never empty — the type and the build both reject one that does not. `customGroups` narrows only the `LOGGED_IN_USERS` half of the set; absent or empty it
72
+ is never empty — the type and the build both reject one that does not. `customGroups` never stands alone: it narrows the `LOGGED_IN_USERS` half of the set, so the rule still names that half, and one without `userGroupTypes` is a type error. Absent or empty it
72
73
  means **any** signed-in visitor, not "nobody".
73
74
 
74
75
  Take every id in `customGroups` from `docs/user-groups.md`, which lists each group the app has with its name and id. An id the app does not have is a type error.
@@ -115,12 +116,6 @@ Every app declares where a denied visitor goes in `softr.json`, at the app root,
115
116
  {
116
117
  "$schema": "./node_modules/@softr/sdk/schema/softr.schema.json",
117
118
  "auth": {
118
- "routes": {
119
- "login": { "path": "/login", "redirectParam": "redirect" },
120
- "resetPassword": { "path": "/reset-password", "tokenParam": "token" },
121
- "acceptInvitation": { "path": "/accept-invitation", "tokenParam": "token" },
122
- "magicLink": { "path": "/magic-authentication", "tokenParam": "magic-token" }
123
- },
124
119
  "guard": {
125
120
  "unauthenticated": "/login",
126
121
  "alreadyAuthenticated": "/",
@@ -130,17 +125,13 @@ Every app declares where a denied visitor goes in `softr.json`, at the app root,
130
125
  }
131
126
  ```
132
127
 
133
- Every key is required, because nothing fills in one the file leaves out:
128
+ All three reasons are required, because nothing fills in a reason the file leaves out:
134
129
 
135
- | Reason | Condition | Goes to |
136
- | --- | --- | --- |
137
- | `unauthenticated` | Route needs a visitor, none present | `guard.unauthenticated` |
138
- | `forbidden` | Signed in, in none of the named groups | `guard.forbidden` |
139
- | `alreadyAuthenticated` | Signed-in visitor on a signed-out-only route | `guard.alreadyAuthenticated` |
140
-
141
- `guard.unauthenticated` is where signed-out visitors go. It is usually the same as `routes.login.path`, but it can be another page, such as a public landing page. Every `unauthenticated` redirect, an override's included, carries the page they wanted under `routes.login.redirectParam`. The login page reads it back with `toReturnPath`. See [auth/SKILL.md](../auth/SKILL.md).
142
-
143
- `auth.routes` names the app's sign-in pages. To move one, rename its route file and edit its `path` in `auth.routes` to match. When `guard.unauthenticated` points at the login page, edit it too.
130
+ | Reason | Condition |
131
+ | --- | --- |
132
+ | `unauthenticated` | Route needs a visitor, none present |
133
+ | `forbidden` | Signed in, in none of the named groups |
134
+ | `alreadyAuthenticated` | Signed-in visitor on a signed-out-only route |
144
135
 
145
136
  That file covers every route at once; a route's own overrides win over it. A rule naming both audiences with no `customGroups` admits everyone, so it never redirects and never throws. Only `strategy: 'throw'` reaches an `errorComponent`. A redirect navigates away and the route never renders.
146
137
 
@@ -172,22 +163,20 @@ Use the predicates: `isAccessError` for any of the three, and `isUnauthenticated
172
163
 
173
164
  ### Run the check and then keep going
174
165
 
175
- `guardRoute` covers almost everything. When a route needs its own logic afterwards, `assertRouteAccess(visibility, opts)` applies the same rule to the whole `beforeLoad` argument and throws the same access error, using the default behaviour for each reason. It takes no overrides.
166
+ `guardRoute` covers almost everything. When a route needs its own logic afterwards, `assertRouteAccess(visibility, context)` applies the same rule and throws the same access error, using the default behaviour for each reason. It takes no overrides.
176
167
 
177
- It also narrows `opts.context.user` for the rest of the handler, where the set names one audience and no other: `User` under `['LOGGED_IN_USERS']`, `null` under `['NON_LOGGED_IN_USERS']`. A set naming both admits either kind of visitor, so it narrows to neither. `guardRoute` cannot, because it hands `beforeLoad` a function rather than running inside one.
168
+ It also narrows `context.user` for the rest of the handler, where the set names one audience and no other: `User` under `['LOGGED_IN_USERS']`, `null` under `['NON_LOGGED_IN_USERS']`. A set naming both admits either kind of visitor, so it narrows to neither. `guardRoute` cannot, because it hands `beforeLoad` a function rather than running inside one.
178
169
 
179
170
  ```ts
180
171
  import { redirect } from '@tanstack/react-router'
181
172
  import { assertRouteAccess } from '@softr/sdk'
182
173
 
183
- beforeLoad: (opts) => {
184
- assertRouteAccess(visibility, opts)
185
- if (opts.context.user.email.endsWith('@example.com')) throw redirect({ to: '/internal' })
174
+ beforeLoad: ({ context }) => {
175
+ assertRouteAccess(visibility, context)
176
+ if (context.user.email.endsWith('@example.com')) throw redirect({ to: '/internal' })
186
177
  },
187
178
  ```
188
179
 
189
- Take `opts` whole. A `context` destructured in the parameter list does not narrow.
190
-
191
180
  Reach for `assertRouteAccess` when the route wants the error thrown for it, `isAuthorized(visibility, context)` when it wants the same narrowing without a throw, and `checkAccess(visibility, context)` when it needs to branch on why. `checkAccess` returns `{ ok: true } | { ok: false, reason }` and throws nothing. All of them are synchronous and make no request.
192
181
 
193
182
  `guardRoute` and `assertRouteAccess` throw router redirects, so they belong in a route and nowhere else. That is what `Route` in their names marks. `isAuthorized` and `checkAccess` throw nothing and answer anywhere, in a loader, a component or a server function.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.64-alpha.1
12
+ library_version: 0.0.61
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.
@@ -1,47 +0,0 @@
1
- import { User } from "../user.js";
2
- import { AuthError, Result } from "./utils.js";
3
-
4
- //#region src/auth/methods/passwordLink.d.ts
5
-
6
- /**
7
- * Emails a link to set a new password.
8
- *
9
- * Succeeds whether or not the address has an account, and sends nothing when
10
- * it has none.
11
- */
12
- declare function requestPasswordReset(details: {
13
- email: string;
14
- }): Promise<Result<null, AuthError>>;
15
- /**
16
- * Checks a password reset or invitation link, and returns the address it was
17
- * mailed to. The link stays usable.
18
- */
19
- declare function checkPasswordLink(details: {
20
- token: string;
21
- }): Promise<Result<{
22
- email: string;
23
- }, AuthError>>;
24
- /**
25
- * Sets a new password with a password reset or invitation link, and signs the
26
- * visitor in.
27
- *
28
- * Any success uses up the link, `authenticatorRequired` included, and ends
29
- * the visitor's other sessions.
30
- */
31
- declare function setPasswordWithLink(details: {
32
- token: string;
33
- password: string;
34
- }): Promise<Result<{
35
- status: "signedIn";
36
- user: User;
37
- } | {
38
- status: "authenticatorRequired";
39
- /**
40
- * `verify`: they have an authenticator app and owe a code.
41
- * `setup`: the app requires one and they have none yet.
42
- * `setupOptional`: same, except they may skip it instead.
43
- */
44
- step: "verify" | "setup" | "setupOptional";
45
- }, AuthError>>;
46
- //#endregion
47
- export { checkPasswordLink, requestPasswordReset, setPasswordWithLink };
@@ -1,86 +0,0 @@
1
- import { assert, panic } from "../../assertions.js";
2
- import { writeCookies } from "../../cookies.js";
3
- import { sessionConfig } from "../config.js";
4
- import { clearPendingAuth, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
5
- import { makePlatform } from "../platform.js";
6
- import { authError } from "./utils.js";
7
-
8
- //#region src/auth/methods/passwordLink.ts
9
- /**
10
- * Emails a link to set a new password.
11
- *
12
- * Succeeds whether or not the address has an account, and sends nothing when
13
- * it has none.
14
- */
15
- async function requestPasswordReset(details) {
16
- const result = await makePlatform(sessionConfig().applicationId).forgotPassword(details);
17
- if (!result.ok) {
18
- if (result.status === 429) return authError("tooManyAttempts", result.message);
19
- panic(`Expected the password reset link to be emailed, but got ${result.status}: ${result.message}.`);
20
- }
21
- return {
22
- success: true,
23
- data: null
24
- };
25
- }
26
- /**
27
- * Checks a password reset or invitation link, and returns the address it was
28
- * mailed to. The link stays usable.
29
- */
30
- async function checkPasswordLink(details) {
31
- const result = await makePlatform(sessionConfig().applicationId).checkPasswordToken(details);
32
- if (!result.ok) {
33
- if (result.status === 429) return authError("tooManyAttempts", result.message);
34
- if (result.status === 400 || result.status === 401) return authError("linkExpired", result.message);
35
- panic(`Expected the link to be checked, but got ${result.status}: ${result.message}.`);
36
- }
37
- return {
38
- success: true,
39
- data: result.data
40
- };
41
- }
42
- /**
43
- * Sets a new password with a password reset or invitation link, and signs the
44
- * visitor in.
45
- *
46
- * Any success uses up the link, `authenticatorRequired` included, and ends
47
- * the visitor's other sessions.
48
- */
49
- async function setPasswordWithLink(details) {
50
- const result = await makePlatform(sessionConfig().applicationId).setPassword(details);
51
- if (!result.ok) {
52
- if (result.status === 429) return authError("tooManyAttempts", result.message);
53
- if (result.status === 400 && result.message === "Password must be at least 8 characters long.") return authError("invalidDetails", result.message);
54
- if (result.status === 400 || result.status === 401) return authError("linkExpired", result.message);
55
- panic(`Expected the password to be set, but got ${result.status}: ${result.message}.`);
56
- }
57
- if (result.data.status === "MFA_REQUIRED") {
58
- const { mfa } = result.data;
59
- assert(mfa, "Expected a challenge from setting a password, but got none");
60
- writeCookies([makePendingAuthCookie(mfa.challengeToken)]);
61
- return {
62
- success: true,
63
- data: {
64
- status: "authenticatorRequired",
65
- step: mfa.action === "VERIFICATION_REQUIRED" ? "verify" : mfa.action === "SETUP_REQUIRED" ? "setup" : "setupOptional"
66
- }
67
- };
68
- }
69
- const { session } = result.data;
70
- assert(session, "Expected a session from setting a password, but got none");
71
- writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
72
- user: session.user,
73
- permissions: session.permissions
74
- }, session.jwt)]);
75
- clearPendingAuth();
76
- return {
77
- success: true,
78
- data: {
79
- status: "signedIn",
80
- user: session.user
81
- }
82
- };
83
- }
84
-
85
- //#endregion
86
- export { checkPasswordLink, requestPasswordReset, setPasswordWithLink };
@@ -1,11 +0,0 @@
1
- import { AuthSettings } from "../platform.js";
2
-
3
- //#region src/auth/methods/settings.d.ts
4
-
5
- /**
6
- * How visitors may sign in and sign up to this app, as configured in Studio.
7
- * A login or sign-up page renders only the methods it lists.
8
- */
9
- declare function settings(): Promise<AuthSettings>;
10
- //#endregion
11
- export { settings };
@@ -1,17 +0,0 @@
1
- import { panic } from "../../assertions.js";
2
- import { sessionConfig } from "../config.js";
3
- import { makePlatform } from "../platform.js";
4
-
5
- //#region src/auth/methods/settings.ts
6
- /**
7
- * How visitors may sign in and sign up to this app, as configured in Studio.
8
- * A login or sign-up page renders only the methods it lists.
9
- */
10
- async function settings() {
11
- const result = await makePlatform(sessionConfig().applicationId).authSettings();
12
- if (!result.ok) panic(`Expected the auth settings, but got ${result.status}: ${result.message}`);
13
- return result.data;
14
- }
15
-
16
- //#endregion
17
- export { settings };
@@ -1,46 +0,0 @@
1
- import "./user.js";
2
- import "./context.js";
3
- import { z } from "zod";
4
-
5
- //#region src/auth/platform.d.ts
6
-
7
- declare const authSettingsSchema: z.ZodObject<{
8
- loginEnabled: z.ZodBoolean;
9
- authenticationMethods: z.ZodArray<z.ZodEnum<{
10
- EMAIL: "EMAIL";
11
- SSO: "SSO";
12
- GOOGLE: "GOOGLE";
13
- MAGIC_LINK: "MAGIC_LINK";
14
- }>>;
15
- signUpPolicy: z.ZodNullable<z.ZodEnum<{
16
- DISABLED: "DISABLED";
17
- OPEN_TO_PUBLIC: "OPEN_TO_PUBLIC";
18
- DOMAIN_RESTRICTED: "DOMAIN_RESTRICTED";
19
- }>>;
20
- sessionExpirationDays: z.ZodNullable<z.ZodNumber>;
21
- inactivityTimeoutMinutes: z.ZodNullable<z.ZodNumber>;
22
- emailAuth: z.ZodNullable<z.ZodObject<{
23
- loginMethods: z.ZodArray<z.ZodEnum<{
24
- PASSWORD: "PASSWORD";
25
- EMAIL_CODE: "EMAIL_CODE";
26
- }>>;
27
- strongPasswords: z.ZodBoolean;
28
- emailCodeExpirationMinutes: z.ZodNullable<z.ZodNumber>;
29
- }, z.core.$strip>>;
30
- googleAuth: z.ZodNullable<z.ZodObject<{
31
- clientId: z.ZodNullable<z.ZodString>;
32
- userType: z.ZodNullable<z.ZodEnum<{
33
- INTERNAL: "INTERNAL";
34
- EXTERNAL: "EXTERNAL";
35
- }>>;
36
- }, z.core.$strip>>;
37
- sso: z.ZodNullable<z.ZodObject<{
38
- type: z.ZodNullable<z.ZodEnum<{
39
- SAML: "SAML";
40
- OPEN_ID: "OPEN_ID";
41
- }>>;
42
- }, z.core.$strip>>;
43
- }, z.core.$strip>;
44
- type AuthSettings = z.infer<typeof authSettingsSchema>;
45
- //#endregion
46
- export { AuthSettings };
@@ -1 +0,0 @@
1
- import { z } from "zod";