@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.
- package/dist/auth/isomorphic.d.ts +4 -63
- package/dist/auth/isomorphic.js +5 -63
- package/dist/auth/methods/signup.js +0 -1
- package/dist/auth/methods/social.js +0 -1
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/platform.js +0 -54
- package/dist/auth/server.d.ts +0 -6
- package/dist/auth/server.js +1 -7
- package/dist/index.d.ts +3 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +8 -2
- package/dist/mcp.js +8 -8
- package/dist/package.js +1 -1
- package/dist/softr-config.js +5 -26
- package/dist/visibility/route.d.ts +6 -34
- package/dist/visibility/route.js +13 -55
- package/package.json +1 -1
- package/schema/softr.schema.json +1 -42
- package/skills/softr-sdk/SKILL.md +3 -4
- package/skills/softr-sdk/auth/SKILL.md +31 -131
- 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 +1 -1
- 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 +14 -7
- 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 +3 -2
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +14 -25
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
- package/dist/auth/methods/passwordLink.d.ts +0 -47
- package/dist/auth/methods/passwordLink.js +0 -86
- package/dist/auth/methods/settings.d.ts +0 -11
- package/dist/auth/methods/settings.js +0 -17
- package/dist/auth/platform.d.ts +0 -46
- 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.
|
|
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
|
|
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
|
-
|
|
128
|
+
All three reasons are required, because nothing fills in a reason the file leaves out:
|
|
134
129
|
|
|
135
|
-
| Reason | Condition |
|
|
136
|
-
| --- | --- |
|
|
137
|
-
| `unauthenticated` | Route needs a visitor, none present |
|
|
138
|
-
| `forbidden` | Signed in, in none of the named groups |
|
|
139
|
-
| `alreadyAuthenticated` | Signed-in visitor on a signed-out-only route |
|
|
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,
|
|
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 `
|
|
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: (
|
|
184
|
-
assertRouteAccess(visibility,
|
|
185
|
-
if (
|
|
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.
|
|
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 };
|
package/dist/auth/platform.d.ts
DELETED
|
@@ -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 };
|
package/dist/softr-config.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|