@softr/sdk 0.0.12 → 0.0.13
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/assertions.js +68 -0
- package/dist/auth/config.js +27 -0
- package/dist/auth/context.d.ts +18 -0
- package/dist/auth/isomorphic.d.ts +73 -0
- package/dist/auth/isomorphic.js +113 -0
- package/dist/auth/methods/password.d.ts +22 -0
- package/dist/auth/methods/password.js +51 -0
- package/dist/auth/methods/signup.d.ts +37 -0
- package/dist/auth/methods/signup.js +89 -0
- package/dist/auth/methods/utils.d.ts +15 -0
- package/dist/auth/methods/utils.js +13 -0
- package/dist/auth/permissions.d.ts +17 -0
- package/dist/auth/permissions.js +13 -0
- package/dist/auth/platform.js +138 -0
- package/dist/auth/server.d.ts +17 -0
- package/dist/auth/server.js +17 -0
- package/dist/auth/session.js +167 -0
- package/dist/auth/user.d.ts +28 -0
- package/dist/client/timezone.d.ts +4 -0
- package/dist/client/timezone.js +11 -0
- package/dist/client.d.ts +1 -4
- package/dist/client.js +1 -9
- package/dist/cookies.js +47 -0
- package/dist/core/datasource.d.ts +21 -0
- package/dist/core/datasource.js +196 -0
- package/dist/core/integration.d.ts +7 -0
- package/dist/core/integration.js +32 -0
- package/dist/{types-n-Sir_Y2.d.ts → core/query.d.ts} +1 -129
- package/dist/core/query.js +528 -0
- package/dist/core/workflow.d.ts +7 -0
- package/dist/core/workflow.js +19 -0
- package/dist/index.d.ts +8 -172
- package/dist/index.js +4 -176
- package/dist/routes/discover.d.ts +9 -0
- package/dist/routes/discover.js +198 -0
- package/dist/routes/manifest.d.ts +17 -0
- package/dist/routes/manifest.js +148 -0
- package/dist/routes/mem-fs.js +71 -0
- package/dist/routes/visibility.d.ts +142 -0
- package/dist/routes/visibility.js +125 -0
- package/dist/server.d.ts +4 -113
- package/dist/server.js +4 -429
- package/dist/types.d.ts +105 -0
- package/dist/vite.d.ts +1 -21
- package/dist/vite.js +2 -405
- package/package.json +4 -4
- package/dist/session-kTqxT5_K.js +0 -891
package/dist/index.d.ts
CHANGED
|
@@ -1,172 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* - `ALL_USERS` — everyone, signed in or not.
|
|
10
|
-
* - `NON_LOGGED_IN_USERS` — signed-out visitors only.
|
|
11
|
-
* - `LOGGED_IN_USERS` — signed-in visitors; with `userGroupIds`, only members of
|
|
12
|
-
* at least one of those groups.
|
|
13
|
-
*/
|
|
14
|
-
type RouteVisibility = {
|
|
15
|
-
userGroup: "ALL_USERS";
|
|
16
|
-
} | {
|
|
17
|
-
userGroup: "NON_LOGGED_IN_USERS";
|
|
18
|
-
} | {
|
|
19
|
-
userGroup: "LOGGED_IN_USERS";
|
|
20
|
-
userGroupIds?: Array<SoftrUserGroupId>;
|
|
21
|
-
};
|
|
22
|
-
/** The route context the root route publishes: the visitor, or `null` when signed out. */
|
|
23
|
-
type SoftrContext = {
|
|
24
|
-
user: User | null;
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Why a visitor was denied.
|
|
28
|
-
*
|
|
29
|
-
* - `unauthenticated` — the route needs a signed-in visitor and there is none.
|
|
30
|
-
* - `forbidden` — signed in, but in none of the groups the route names.
|
|
31
|
-
* - `alreadySignedIn` — the route is for signed-out visitors only.
|
|
32
|
-
*/
|
|
33
|
-
declare const ACCESS_REASONS: readonly ["unauthenticated", "forbidden", "alreadySignedIn"];
|
|
34
|
-
type AccessReason = (typeof ACCESS_REASONS)[number];
|
|
35
|
-
/**
|
|
36
|
-
* What a guard does when it refuses a visitor: redirect to a path, or throw an
|
|
37
|
-
* {@link AccessError} for the route's `errorComponent` to render. A bare string
|
|
38
|
-
* is shorthand for redirecting there.
|
|
39
|
-
*/
|
|
40
|
-
type AccessBehaviour = string | {
|
|
41
|
-
strategy: "redirect";
|
|
42
|
-
to: string;
|
|
43
|
-
} | {
|
|
44
|
-
strategy: "throw";
|
|
45
|
-
};
|
|
46
|
-
/** Per-reason overrides of the default {@link AccessBehaviour}. */
|
|
47
|
-
type GuardOverrides = Partial<Record<AccessReason, AccessBehaviour>>;
|
|
48
|
-
/** The result of {@link VisibilityHandle.check}: passed, or denied with a reason. */
|
|
49
|
-
type AccessCheck = {
|
|
50
|
-
ok: true;
|
|
51
|
-
} | {
|
|
52
|
-
ok: false;
|
|
53
|
-
reason: AccessReason;
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* The access error a guard throws. It is plain data, not an `Error` subclass, so
|
|
57
|
-
* take it as `unknown` in an `errorComponent` and narrow it with
|
|
58
|
-
* {@link isAccessError}. It carries the reason and nothing else: the error is
|
|
59
|
-
* serialized into the page, so it never names the groups the route wanted.
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* errorComponent: ({ error }: { error: unknown }) =>
|
|
63
|
-
* isAccessError(error) ? <NoAccess /> : <AppError />
|
|
64
|
-
*/
|
|
65
|
-
type AccessError = {
|
|
66
|
-
type: "access_error";
|
|
67
|
-
reason: AccessReason;
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* True for any access error thrown by a guard, whatever its reason.
|
|
71
|
-
*
|
|
72
|
-
* An access error arrives as `unknown` — hydrated from the page, or thrown by
|
|
73
|
-
* anything else at all — so every field is checked before it is narrowed.
|
|
74
|
-
*/
|
|
75
|
-
declare function isAccessError(error: unknown): error is AccessError;
|
|
76
|
-
/** True for an access error thrown because nobody was signed in. */
|
|
77
|
-
declare function isUnauthenticatedError(error: unknown): error is AccessError;
|
|
78
|
-
/** True for an access error thrown because the visitor was in none of the groups. */
|
|
79
|
-
declare function isForbiddenError(error: unknown): error is AccessError;
|
|
80
|
-
/** True for an access error thrown because a signed-out-only route saw a user. */
|
|
81
|
-
declare function isAlreadySignedInError(error: unknown): error is AccessError;
|
|
82
|
-
type Narrowed<R extends RouteVisibility> = R extends {
|
|
83
|
-
userGroup: "LOGGED_IN_USERS";
|
|
84
|
-
} ? SoftrContext & {
|
|
85
|
-
user: User;
|
|
86
|
-
} : R extends {
|
|
87
|
-
userGroup: "NON_LOGGED_IN_USERS";
|
|
88
|
-
} ? SoftrContext & {
|
|
89
|
-
user: null;
|
|
90
|
-
} : SoftrContext;
|
|
91
|
-
/** The four ways to apply a rule, returned by {@link defineVisibility}. */
|
|
92
|
-
type VisibilityHandle<R extends RouteVisibility = RouteVisibility> = {
|
|
93
|
-
/**
|
|
94
|
-
* A `beforeLoad` handler that denies a visitor who fails the rule.
|
|
95
|
-
*
|
|
96
|
-
* By default an unauthenticated visitor is redirected to `/login`, an already
|
|
97
|
-
* signed-in one to `/`, and a forbidden one gets an {@link AccessError} thrown
|
|
98
|
-
* in place, for the route's `errorComponent` to render. Pass `overrides` to
|
|
99
|
-
* change what any one reason does.
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* beforeLoad: visibility.guard({ unauthenticated: "/sign-in" })
|
|
103
|
-
*/
|
|
104
|
-
guard(overrides?: GuardOverrides): (opts: {
|
|
105
|
-
context: SoftrContext;
|
|
106
|
-
}) => void;
|
|
107
|
-
/**
|
|
108
|
-
* Throws the same access error or redirect `guard()` would, for code that
|
|
109
|
-
* needs to run its own checks after the rule.
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* beforeLoad: ({ context }) => {
|
|
113
|
-
* visibility.assert(context)
|
|
114
|
-
* if (context.user.trialExpired) throw redirect({ to: "/renew" })
|
|
115
|
-
* }
|
|
116
|
-
*/
|
|
117
|
-
assert(context: SoftrContext): void;
|
|
118
|
-
/**
|
|
119
|
-
* Whether the visitor passes the rule. Narrows `context.user` where the rule
|
|
120
|
-
* allows it.
|
|
121
|
-
*/
|
|
122
|
-
isAuthorized(context: SoftrContext): context is Narrowed<R>;
|
|
123
|
-
/** The decision and, when a visitor is refused, the reason. Throws nothing. */
|
|
124
|
-
check(context: SoftrContext): AccessCheck;
|
|
125
|
-
};
|
|
126
|
-
/**
|
|
127
|
-
* Declares who may see a route, and returns the handle that enforces it.
|
|
128
|
-
*
|
|
129
|
-
* One call per route file, at module scope: the build reads these declarations
|
|
130
|
-
* to publish the app's route visibility, so the rule must be a literal.
|
|
131
|
-
*
|
|
132
|
-
* @example
|
|
133
|
-
* const visibility = defineVisibility({ userGroup: "LOGGED_IN_USERS" })
|
|
134
|
-
*
|
|
135
|
-
* export const Route = createFileRoute("/about")({
|
|
136
|
-
* beforeLoad: visibility.guard(),
|
|
137
|
-
* component: About,
|
|
138
|
-
* })
|
|
139
|
-
*/
|
|
140
|
-
declare function defineVisibility<const R extends RouteVisibility>(rule: R): VisibilityHandle<R>;
|
|
141
|
-
//#endregion
|
|
142
|
-
//#region src/auth/isomorphic.d.ts
|
|
143
|
-
/**
|
|
144
|
-
* Resolves the Softr visitor for every request, so that `getSoftrUser` and the
|
|
145
|
-
* route guards have someone to check.
|
|
146
|
-
*
|
|
147
|
-
* Register it once, as a global request middleware in the app's `src/start.ts`.
|
|
148
|
-
* It touches nothing but the visitor.
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* export const startInstance = createStart(() => ({
|
|
152
|
-
* requestMiddleware: [softrAuthMiddleware()],
|
|
153
|
-
* }))
|
|
154
|
-
*/
|
|
155
|
-
declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, {
|
|
156
|
-
user: User | null;
|
|
157
|
-
}>;
|
|
158
|
-
/**
|
|
159
|
-
* Returns the signed-in visitor, or `null` when nobody is signed in.
|
|
160
|
-
*
|
|
161
|
-
* Await it in the root route's `beforeLoad` to publish the visitor as route
|
|
162
|
-
* context, which every `defineVisibility` guard then reads. It requires
|
|
163
|
-
* {@link softrAuthMiddleware} to be registered.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* export const Route = createRootRouteWithContext<SoftrContext>()({
|
|
167
|
-
* beforeLoad: async () => ({ user: await getSoftrUser() }),
|
|
168
|
-
* })
|
|
169
|
-
*/
|
|
170
|
-
declare const getSoftrUser: _tanstack_react_start0.OptionalFetcher<undefined, undefined, Promise<User | null>>;
|
|
171
|
-
//#endregion
|
|
172
|
-
export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartDataOptions, type ChartDataResult, type CreateOptions, type DatasourcePermissions, type GetOptions, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrUser, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware };
|
|
1
|
+
import { q } from "./core/query.js";
|
|
2
|
+
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
3
|
+
import { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions } from "./types.js";
|
|
4
|
+
import { Permissions } from "./auth/permissions.js";
|
|
5
|
+
import { SoftrContext } from "./auth/context.js";
|
|
6
|
+
import { AccessBehaviour, AccessCheck, AccessError, AccessReason, RouteVisibility, VisibilityHandle, defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
7
|
+
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
8
|
+
export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartDataOptions, type ChartDataResult, type CreateOptions, type DatasourcePermissions, type GetOptions, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type Permissions, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions };
|
package/dist/index.js
CHANGED
|
@@ -1,177 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { q } from "./core/query.js";
|
|
2
|
+
import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
3
|
+
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Why a visitor was denied.
|
|
8
|
-
*
|
|
9
|
-
* - `unauthenticated` — the route needs a signed-in visitor and there is none.
|
|
10
|
-
* - `forbidden` — signed in, but in none of the groups the route names.
|
|
11
|
-
* - `alreadySignedIn` — the route is for signed-out visitors only.
|
|
12
|
-
*/
|
|
13
|
-
const ACCESS_REASONS = [
|
|
14
|
-
"unauthenticated",
|
|
15
|
-
"forbidden",
|
|
16
|
-
"alreadySignedIn"
|
|
17
|
-
];
|
|
18
|
-
/** Decides whether a visitor passes a visibility rule, without throwing. */
|
|
19
|
-
function checkAccess(rule, context) {
|
|
20
|
-
const { user } = context;
|
|
21
|
-
switch (rule.userGroup) {
|
|
22
|
-
case "ALL_USERS": return { ok: true };
|
|
23
|
-
case "NON_LOGGED_IN_USERS": return user === null ? { ok: true } : {
|
|
24
|
-
ok: false,
|
|
25
|
-
reason: "alreadySignedIn"
|
|
26
|
-
};
|
|
27
|
-
case "LOGGED_IN_USERS": {
|
|
28
|
-
if (user === null) return {
|
|
29
|
-
ok: false,
|
|
30
|
-
reason: "unauthenticated"
|
|
31
|
-
};
|
|
32
|
-
const required = rule.userGroupIds ?? [];
|
|
33
|
-
if (required.length === 0) return { ok: true };
|
|
34
|
-
return required.some((id) => user.userGroupIds.includes(id)) ? { ok: true } : {
|
|
35
|
-
ok: false,
|
|
36
|
-
reason: "forbidden"
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function isAccessReason(value) {
|
|
42
|
-
return ACCESS_REASONS.some((reason) => reason === value);
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* True for any access error thrown by a guard, whatever its reason.
|
|
46
|
-
*
|
|
47
|
-
* An access error arrives as `unknown` — hydrated from the page, or thrown by
|
|
48
|
-
* anything else at all — so every field is checked before it is narrowed.
|
|
49
|
-
*/
|
|
50
|
-
function isAccessError(error) {
|
|
51
|
-
if (typeof error !== "object" || error === null) return false;
|
|
52
|
-
const candidate = error;
|
|
53
|
-
if (candidate.type !== "access_error") return false;
|
|
54
|
-
return isAccessReason(candidate.reason);
|
|
55
|
-
}
|
|
56
|
-
function isReason(error, reason) {
|
|
57
|
-
return isAccessError(error) && error.reason === reason;
|
|
58
|
-
}
|
|
59
|
-
/** True for an access error thrown because nobody was signed in. */
|
|
60
|
-
function isUnauthenticatedError(error) {
|
|
61
|
-
return isReason(error, "unauthenticated");
|
|
62
|
-
}
|
|
63
|
-
/** True for an access error thrown because the visitor was in none of the groups. */
|
|
64
|
-
function isForbiddenError(error) {
|
|
65
|
-
return isReason(error, "forbidden");
|
|
66
|
-
}
|
|
67
|
-
/** True for an access error thrown because a signed-out-only route saw a user. */
|
|
68
|
-
function isAlreadySignedInError(error) {
|
|
69
|
-
return isReason(error, "alreadySignedIn");
|
|
70
|
-
}
|
|
71
|
-
const DEFAULT_ACCESS_BEHAVIOUR = {
|
|
72
|
-
unauthenticated: {
|
|
73
|
-
strategy: "redirect",
|
|
74
|
-
to: "/login"
|
|
75
|
-
},
|
|
76
|
-
forbidden: { strategy: "throw" },
|
|
77
|
-
alreadySignedIn: {
|
|
78
|
-
strategy: "redirect",
|
|
79
|
-
to: "/"
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
/** What a guard throws for a reason: a redirect, or an {@link AccessError}. */
|
|
83
|
-
function thrownFor(reason, overrides) {
|
|
84
|
-
const behaviour = overrides?.[reason] ?? DEFAULT_ACCESS_BEHAVIOUR[reason];
|
|
85
|
-
const resolved = typeof behaviour === "string" ? {
|
|
86
|
-
strategy: "redirect",
|
|
87
|
-
to: behaviour
|
|
88
|
-
} : behaviour;
|
|
89
|
-
if (resolved.strategy === "redirect") return redirect({ to: resolved.to });
|
|
90
|
-
return {
|
|
91
|
-
type: "access_error",
|
|
92
|
-
reason
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Declares who may see a route, and returns the handle that enforces it.
|
|
97
|
-
*
|
|
98
|
-
* One call per route file, at module scope: the build reads these declarations
|
|
99
|
-
* to publish the app's route visibility, so the rule must be a literal.
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* const visibility = defineVisibility({ userGroup: "LOGGED_IN_USERS" })
|
|
103
|
-
*
|
|
104
|
-
* export const Route = createFileRoute("/about")({
|
|
105
|
-
* beforeLoad: visibility.guard(),
|
|
106
|
-
* component: About,
|
|
107
|
-
* })
|
|
108
|
-
*/
|
|
109
|
-
function defineVisibility(rule) {
|
|
110
|
-
return {
|
|
111
|
-
guard: (overrides) => ({ context }) => {
|
|
112
|
-
const result = checkAccess(rule, context);
|
|
113
|
-
if (result.ok) return;
|
|
114
|
-
throw thrownFor(result.reason, overrides);
|
|
115
|
-
},
|
|
116
|
-
assert: (context) => {
|
|
117
|
-
const result = checkAccess(rule, context);
|
|
118
|
-
if (result.ok) return;
|
|
119
|
-
throw thrownFor(result.reason, void 0);
|
|
120
|
-
},
|
|
121
|
-
isAuthorized: (context) => checkAccess(rule, context).ok,
|
|
122
|
-
check: (context) => checkAccess(rule, context)
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
//#endregion
|
|
127
|
-
//#region src/auth/isomorphic.ts
|
|
128
|
-
/**
|
|
129
|
-
* Resolves the Softr visitor for every request, so that `getSoftrUser` and the
|
|
130
|
-
* route guards have someone to check.
|
|
131
|
-
*
|
|
132
|
-
* Register it once, as a global request middleware in the app's `src/start.ts`.
|
|
133
|
-
* It touches nothing but the visitor.
|
|
134
|
-
*
|
|
135
|
-
* @example
|
|
136
|
-
* export const startInstance = createStart(() => ({
|
|
137
|
-
* requestMiddleware: [softrAuthMiddleware()],
|
|
138
|
-
* }))
|
|
139
|
-
*/
|
|
140
|
-
function softrAuthMiddleware() {
|
|
141
|
-
return createMiddleware({ type: "request" }).server(async ({ next }) => {
|
|
142
|
-
async function getUser() {
|
|
143
|
-
const token = readSessionToken();
|
|
144
|
-
if (token === null) return null;
|
|
145
|
-
const cached = readSessionData(token);
|
|
146
|
-
if (cached !== null) return cached;
|
|
147
|
-
const user = await fetchVisitor(token);
|
|
148
|
-
if (user === null) {
|
|
149
|
-
clearSession();
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
writeCookies([makeSessionDataCookie(user, token)]);
|
|
153
|
-
return user;
|
|
154
|
-
}
|
|
155
|
-
return next({ context: { user: await getUser() } });
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Returns the signed-in visitor, or `null` when nobody is signed in.
|
|
160
|
-
*
|
|
161
|
-
* Await it in the root route's `beforeLoad` to publish the visitor as route
|
|
162
|
-
* context, which every `defineVisibility` guard then reads. It requires
|
|
163
|
-
* {@link softrAuthMiddleware} to be registered.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* export const Route = createRootRouteWithContext<SoftrContext>()({
|
|
167
|
-
* beforeLoad: async () => ({ user: await getSoftrUser() }),
|
|
168
|
-
* })
|
|
169
|
-
*/
|
|
170
|
-
const getSoftrUser = createServerFn({ method: "GET" }).handler(async () => {
|
|
171
|
-
const context = getGlobalStartContext();
|
|
172
|
-
assert(typeof context === "object" && context !== null && "user" in context, "softrAuthMiddleware has not run: the Start global context carries no `user`. Register it as a global request middleware, e.g. createStart(() => ({ requestMiddleware: [softrAuthMiddleware()] })).");
|
|
173
|
-
return context.user ?? null;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
//#endregion
|
|
177
|
-
export { defineVisibility, getSoftrUser, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware };
|
|
5
|
+
export { defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createReadOnlyFs } from "./mem-fs.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Generator, getConfig } from "@tanstack/router-generator";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { parse } from "@babel/parser";
|
|
7
|
+
import babelTraverse from "@babel/traverse";
|
|
8
|
+
|
|
9
|
+
//#region src/routes/discover.ts
|
|
10
|
+
async function crawlRoutes(opts) {
|
|
11
|
+
const config = getConfig({
|
|
12
|
+
disableLogging: true,
|
|
13
|
+
tmpDir: TMP_DIR,
|
|
14
|
+
generatedRouteTree: generatedRouteTreePath()
|
|
15
|
+
}, opts.root);
|
|
16
|
+
const generator = new Generator({
|
|
17
|
+
config,
|
|
18
|
+
root: opts.root,
|
|
19
|
+
fs: createReadOnlyFs()
|
|
20
|
+
});
|
|
21
|
+
await generator.run();
|
|
22
|
+
const crawlingResult = await generator.getCrawlingResult();
|
|
23
|
+
if (!crawlingResult) throw new Error(`the route crawl produced no result for ${opts.root}`);
|
|
24
|
+
return {
|
|
25
|
+
rootRouteNode: crawlingResult.rootRouteNode,
|
|
26
|
+
routeNodes: crawlingResult.acc.routeNodes,
|
|
27
|
+
routePiecesByPath: crawlingResult.acc.routePiecesByPath,
|
|
28
|
+
routeTree: crawlingResult.acc.routeTree,
|
|
29
|
+
routesDirectory: path.relative(opts.root, path.resolve(opts.root, config.routesDirectory)).split(path.sep).join("/")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Reads the rule out of the source text. The route file is never executed. */
|
|
33
|
+
function extractVisibility(source, filePath) {
|
|
34
|
+
let program;
|
|
35
|
+
try {
|
|
36
|
+
program = parse(source, {
|
|
37
|
+
sourceType: "module",
|
|
38
|
+
plugins: ["typescript", "jsx"]
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const position = error.loc;
|
|
42
|
+
return fail("unparsable", `could not parse ${filePath}: ${error.message}`, filePath, position?.line ?? 1, position?.column ?? 0);
|
|
43
|
+
}
|
|
44
|
+
const calls = findVisibilityCalls(program);
|
|
45
|
+
if (calls.length > 1) {
|
|
46
|
+
const second = calls[1].call;
|
|
47
|
+
return fail("duplicate-declaration", `${filePath} declares visibility more than once; a route has exactly one rule`, filePath, second.loc?.start.line ?? 1, second.loc?.start.column ?? 0);
|
|
48
|
+
}
|
|
49
|
+
const declaration = calls[0];
|
|
50
|
+
if (declaration === void 0) return {
|
|
51
|
+
ok: true,
|
|
52
|
+
visibility: null
|
|
53
|
+
};
|
|
54
|
+
const { call, statement } = declaration;
|
|
55
|
+
const argument = call.arguments[0];
|
|
56
|
+
if (argument === void 0) return fail("not-literal", `${filePath} calls ${VISIBILITY_FUNCTION} with no argument`, filePath, call.loc?.start.line ?? 1, call.loc?.start.column ?? 0);
|
|
57
|
+
const read = readLiteral(argument);
|
|
58
|
+
if (!read.ok) return fail("not-literal", `${filePath}: the visibility rule must be a literal — ${read.reason}`, filePath, read.line, read.column);
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
visibility: {
|
|
62
|
+
value: read.value,
|
|
63
|
+
line: statement.loc?.start.line ?? 1,
|
|
64
|
+
column: statement.loc?.start.column ?? 0
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The generator `mkdirSync`s `tmpDir` and the directory of `generatedRouteTree` outside the `fs`
|
|
70
|
+
* shim, so defaulted they create directories in the crawled project. Aimed at `os.tmpdir()` both
|
|
71
|
+
* calls are no-ops, and nothing is ever written through either path. The uuid keeps the generated
|
|
72
|
+
* tree file a miss rather than whatever a shared temp directory happens to hold.
|
|
73
|
+
*/
|
|
74
|
+
const TMP_DIR = os.tmpdir();
|
|
75
|
+
const generatedRouteTreePath = () => path.join(TMP_DIR, `softr-sdk-route-tree-${randomUUID()}.gen.ts`);
|
|
76
|
+
const SDK_PACKAGE_NAME = "@softr/sdk";
|
|
77
|
+
const VISIBILITY_FUNCTION = "defineVisibility";
|
|
78
|
+
function fail(code, message, filePath, line, column) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: {
|
|
82
|
+
code,
|
|
83
|
+
message,
|
|
84
|
+
filePath,
|
|
85
|
+
line,
|
|
86
|
+
column
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const traverse = babelTraverse.default ?? babelTraverse;
|
|
91
|
+
/** Only a call initialising a top-level variable counts; one inside a function or block does not. */
|
|
92
|
+
function findVisibilityCalls(ast) {
|
|
93
|
+
const found = [];
|
|
94
|
+
traverse(ast, { VariableDeclarator(path$1) {
|
|
95
|
+
const list = path$1.parentPath;
|
|
96
|
+
if (!list.isVariableDeclaration()) return;
|
|
97
|
+
const statement = list.parentPath.isExportNamedDeclaration() ? list.parentPath : list;
|
|
98
|
+
if (!statement.parentPath?.isProgram()) return;
|
|
99
|
+
if (path$1.node.init === null || path$1.node.init === void 0) return;
|
|
100
|
+
const call = unwrap(path$1.node.init);
|
|
101
|
+
if (call.type !== "CallExpression" && call.type !== "OptionalCallExpression") return;
|
|
102
|
+
if (call.callee.type !== "Identifier") return;
|
|
103
|
+
if (!isVisibilityImport(path$1, call.callee.name)) return;
|
|
104
|
+
found.push({
|
|
105
|
+
call,
|
|
106
|
+
statement: statement.node
|
|
107
|
+
});
|
|
108
|
+
} });
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
111
|
+
function isVisibilityImport(path$1, name) {
|
|
112
|
+
const binding = path$1.scope.getBinding(name);
|
|
113
|
+
if (binding === void 0 || binding.kind !== "module") return false;
|
|
114
|
+
const specifier = binding.path.node;
|
|
115
|
+
const declaration = binding.path.parent;
|
|
116
|
+
if (specifier.type !== "ImportSpecifier" || declaration.type !== "ImportDeclaration") return false;
|
|
117
|
+
if (specifier.importKind === "type" || declaration.importKind === "type") return false;
|
|
118
|
+
if (declaration.source.value !== SDK_PACKAGE_NAME) return false;
|
|
119
|
+
return (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) === VISIBILITY_FUNCTION;
|
|
120
|
+
}
|
|
121
|
+
/** Strips the wrappers that leave the runtime value unchanged: `as const`, `satisfies`, `!`, `()`. */
|
|
122
|
+
function unwrap(node) {
|
|
123
|
+
switch (node.type) {
|
|
124
|
+
case "TSAsExpression":
|
|
125
|
+
case "TSSatisfiesExpression":
|
|
126
|
+
case "TSNonNullExpression":
|
|
127
|
+
case "TSTypeAssertion":
|
|
128
|
+
case "TSInstantiationExpression":
|
|
129
|
+
case "ParenthesizedExpression": return unwrap(node.expression);
|
|
130
|
+
default: return node;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function reject(node, reason) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason,
|
|
137
|
+
line: node.loc?.start.line ?? 1,
|
|
138
|
+
column: node.loc?.start.column ?? 0
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function readLiteral(input) {
|
|
142
|
+
const node = unwrap(input);
|
|
143
|
+
switch (node.type) {
|
|
144
|
+
case "StringLiteral": return {
|
|
145
|
+
ok: true,
|
|
146
|
+
value: node.value
|
|
147
|
+
};
|
|
148
|
+
case "TemplateLiteral":
|
|
149
|
+
if (node.expressions.length > 0) return reject(node, "a template literal with an expression is not a static value");
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
value: node.quasis[0]?.value.cooked ?? ""
|
|
153
|
+
};
|
|
154
|
+
case "ArrayExpression": {
|
|
155
|
+
const values = [];
|
|
156
|
+
for (const element of node.elements) {
|
|
157
|
+
if (element === null) return reject(node, "an array hole is not a static value");
|
|
158
|
+
if (element.type === "SpreadElement") return reject(element, "a spread is not a static value");
|
|
159
|
+
const read = readLiteral(element);
|
|
160
|
+
if (!read.ok) return read;
|
|
161
|
+
values.push(read.value);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
value: values
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
case "ObjectExpression": {
|
|
169
|
+
const value = {};
|
|
170
|
+
for (const property of node.properties) {
|
|
171
|
+
if (property.type === "SpreadElement") return reject(property, "a spread is not a static value");
|
|
172
|
+
if (property.type === "ObjectMethod") return reject(property, "a method is not a static value");
|
|
173
|
+
if (property.computed) return reject(property, "a computed key is not a static value");
|
|
174
|
+
if (property.shorthand) return reject(property, "a shorthand property is not a static value");
|
|
175
|
+
const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "StringLiteral" ? property.key.value : null;
|
|
176
|
+
if (key === null) return reject(property.key, "this key is not a static value");
|
|
177
|
+
if (key === "__proto__") return reject(property.key, "a __proto__ key is not a static value");
|
|
178
|
+
const read = readLiteral(property.value);
|
|
179
|
+
if (!read.ok) return read;
|
|
180
|
+
value[key] = read.value;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
value
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
case "NumericLiteral":
|
|
188
|
+
case "BooleanLiteral":
|
|
189
|
+
case "NullLiteral": return reject(node, "a visibility rule is built from strings, arrays and objects");
|
|
190
|
+
case "Identifier": return reject(node, `an identifier (${node.name}) is not a static value`);
|
|
191
|
+
case "CallExpression":
|
|
192
|
+
case "NewExpression": return reject(node, "a call is not a static value");
|
|
193
|
+
default: return reject(node, `${node.type} is not a static value`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
export { crawlRoutes, extractVisibility };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { VisibilityValue } from "./discover.js";
|
|
2
|
+
|
|
3
|
+
//#region src/routes/manifest.d.ts
|
|
4
|
+
type ManifestNode = {
|
|
5
|
+
routePath: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
_fsRouteType?: string;
|
|
8
|
+
inferFullPath: string | null;
|
|
9
|
+
createFileRouteProps: Array<string>;
|
|
10
|
+
visibility: VisibilityValue | null;
|
|
11
|
+
children: Array<ManifestNode>;
|
|
12
|
+
};
|
|
13
|
+
type Manifest = {
|
|
14
|
+
root: ManifestNode;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { Manifest, ManifestNode };
|