@softr/sdk 0.0.11 → 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.
Files changed (47) hide show
  1. package/dist/assertions.js +68 -0
  2. package/dist/auth/config.js +27 -0
  3. package/dist/auth/context.d.ts +18 -0
  4. package/dist/auth/isomorphic.d.ts +73 -0
  5. package/dist/auth/isomorphic.js +113 -0
  6. package/dist/auth/methods/password.d.ts +22 -0
  7. package/dist/auth/methods/password.js +51 -0
  8. package/dist/auth/methods/signup.d.ts +37 -0
  9. package/dist/auth/methods/signup.js +89 -0
  10. package/dist/auth/methods/utils.d.ts +15 -0
  11. package/dist/auth/methods/utils.js +13 -0
  12. package/dist/auth/permissions.d.ts +17 -0
  13. package/dist/auth/permissions.js +13 -0
  14. package/dist/auth/platform.js +138 -0
  15. package/dist/auth/server.d.ts +17 -0
  16. package/dist/auth/server.js +17 -0
  17. package/dist/auth/session.js +167 -0
  18. package/dist/{user-D23hwTsf.d.ts → auth/user.d.ts} +1 -1
  19. package/dist/client/timezone.d.ts +4 -0
  20. package/dist/client/timezone.js +11 -0
  21. package/dist/client.d.ts +1 -4
  22. package/dist/client.js +1 -9
  23. package/dist/cookies.js +47 -0
  24. package/dist/core/datasource.d.ts +21 -0
  25. package/dist/core/datasource.js +196 -0
  26. package/dist/core/integration.d.ts +7 -0
  27. package/dist/core/integration.js +32 -0
  28. package/dist/core/query.d.ts +354 -0
  29. package/dist/core/query.js +528 -0
  30. package/dist/core/workflow.d.ts +7 -0
  31. package/dist/core/workflow.js +19 -0
  32. package/dist/index.d.ts +8 -685
  33. package/dist/index.js +4 -1019
  34. package/dist/routes/discover.d.ts +9 -0
  35. package/dist/routes/discover.js +198 -0
  36. package/dist/routes/manifest.d.ts +17 -0
  37. package/dist/routes/manifest.js +148 -0
  38. package/dist/routes/mem-fs.js +71 -0
  39. package/dist/routes/visibility.d.ts +142 -0
  40. package/dist/routes/visibility.js +125 -0
  41. package/dist/server.d.ts +5 -84
  42. package/dist/server.js +5 -194
  43. package/dist/types.d.ts +105 -0
  44. package/dist/vite.d.ts +1 -21
  45. package/dist/vite.js +2 -405
  46. package/package.json +4 -4
  47. package/dist/session-GUEI96C8.js +0 -360
@@ -0,0 +1,9 @@
1
+ import "@tanstack/router-generator";
2
+
3
+ //#region src/routes/discover.d.ts
4
+
5
+ type VisibilityValue = string | Array<VisibilityValue> | {
6
+ [key: string]: VisibilityValue;
7
+ };
8
+ //#endregion
9
+ export { VisibilityValue };
@@ -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 };
@@ -0,0 +1,148 @@
1
+ import { crawlRoutes, extractVisibility } from "./discover.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { inferFullPath } from "@tanstack/router-generator";
5
+
6
+ //#region src/routes/manifest.ts
7
+ var ManifestError = class extends Error {
8
+ detail;
9
+ constructor(detail) {
10
+ super(detail.message);
11
+ this.name = "ManifestError";
12
+ this.detail = detail;
13
+ }
14
+ };
15
+ /** Returns the manifest as the exact text to write. Throws on a declaration it cannot read. */
16
+ async function generateRoutesManifest(root) {
17
+ const crawl = await crawlRoutes({ root });
18
+ const routeFiles = new Set([crawl.rootRouteNode.filePath]);
19
+ for (const node of crawl.routeNodes) routeFiles.add(node.filePath);
20
+ for (const pieces of Object.values(crawl.routePiecesByPath)) if (pieces.lazy) routeFiles.add(pieces.lazy.filePath);
21
+ const sources = /* @__PURE__ */ new Map();
22
+ for (const relative of routeFiles) {
23
+ const absolute = path.resolve(root, crawl.routesDirectory, relative);
24
+ const source = await readIfPresent(absolute);
25
+ if (source !== null) sources.set(absolute, source);
26
+ }
27
+ const manifest = buildManifest({
28
+ crawl,
29
+ root,
30
+ sources
31
+ });
32
+ return `${JSON.stringify(manifest, null, 2)}\n`;
33
+ }
34
+ function buildManifest(opts) {
35
+ const routesDirectory = opts.crawl.routesDirectory;
36
+ const toNode = (node) => {
37
+ const projectRelative = toProjectRelative(opts.root, routesDirectory, node.filePath);
38
+ const routePath = node.routePath;
39
+ if (routePath === void 0) throw new Error(`the route crawl produced a node with no route path: ${projectRelative}`);
40
+ let declared = visibilityOf(opts, node.filePath);
41
+ const lazyPiece = opts.crawl.routePiecesByPath[routePath]?.lazy;
42
+ if (lazyPiece && lazyPiece.filePath !== node.filePath) {
43
+ const lazyDeclared = visibilityOf(opts, lazyPiece.filePath);
44
+ if (lazyDeclared !== null && declared !== null) throw new ManifestError({
45
+ code: "duplicate-declaration",
46
+ message: `${routePath} declares visibility in both ${projectRelative} and ${lazyDeclared.filePath}; a route has exactly one rule`,
47
+ filePath: lazyDeclared.filePath,
48
+ line: lazyDeclared.line,
49
+ column: lazyDeclared.column
50
+ });
51
+ if (lazyDeclared !== null) declared = lazyDeclared;
52
+ }
53
+ return {
54
+ routePath,
55
+ filePath: projectRelative,
56
+ ...node._fsRouteType ? { _fsRouteType: node._fsRouteType } : {},
57
+ inferFullPath: inferFullPath(node) ?? null,
58
+ createFileRouteProps: [...node.createFileRouteProps ?? []].sort(),
59
+ visibility: declared?.value ?? null,
60
+ children: (node.children ?? []).map(toNode)
61
+ };
62
+ };
63
+ const rootNode = opts.crawl.rootRouteNode;
64
+ const rootRelative = toProjectRelative(opts.root, routesDirectory, rootNode.filePath);
65
+ const rootDeclared = visibilityOf(opts, rootNode.filePath);
66
+ if (rootDeclared !== null) throw new ManifestError({
67
+ code: "root-declaration",
68
+ message: "the root route cannot carry a visibility rule: it would gate sign-in with no way back in. Move this declaration to the routes you want gated.",
69
+ filePath: rootRelative,
70
+ line: rootDeclared.line,
71
+ column: rootDeclared.column
72
+ });
73
+ return { root: {
74
+ routePath: ROOT_ROUTE_ID,
75
+ filePath: rootRelative,
76
+ inferFullPath: inferFullPath(rootNode) ?? null,
77
+ createFileRouteProps: [...rootNode.createFileRouteProps ?? []].sort(),
78
+ visibility: null,
79
+ children: opts.crawl.routeTree.map(toNode)
80
+ } };
81
+ }
82
+ /**
83
+ * `rootRouteId` from `@tanstack/router-core`, which is what a consumer matches against — not the
84
+ * generator's `rootPathId`, which is the base name of `__root.tsx` and names a file, not a route.
85
+ */
86
+ const ROOT_ROUTE_ID = "__root__";
87
+ /** The crawl's paths are relative to the routes directory; the manifest's are project-relative. */
88
+ function toProjectRelative(root, routesDirectory, filePath) {
89
+ const absolute = path.resolve(root, routesDirectory, filePath);
90
+ return path.relative(root, absolute).split(path.sep).join("/");
91
+ }
92
+ const USER_GROUPS = [
93
+ "ALL_USERS",
94
+ "NON_LOGGED_IN_USERS",
95
+ "LOGGED_IN_USERS"
96
+ ];
97
+ function checkVisibilityShape(declared) {
98
+ const reject = (message) => {
99
+ throw new ManifestError({
100
+ code: "invalid-visibility",
101
+ message: `${declared.filePath}: ${message}`,
102
+ filePath: declared.filePath,
103
+ line: declared.line,
104
+ column: declared.column
105
+ });
106
+ };
107
+ const visibility = declared.value;
108
+ if (typeof visibility !== "object" || Array.isArray(visibility)) reject(`the visibility rule must be an object literal like { userGroup: 'ALL_USERS' }, not ${JSON.stringify(visibility)}`);
109
+ for (const key of Object.keys(visibility)) {
110
+ if (key === "userGroup" || key === "userGroupIds") continue;
111
+ reject(`"${key}" is not part of a visibility rule; a rule has userGroup and, under LOGGED_IN_USERS, userGroupIds. Check the spelling.`);
112
+ }
113
+ const userGroup = visibility["userGroup"];
114
+ if (typeof userGroup !== "string" || !USER_GROUPS.includes(userGroup)) reject(`userGroup must be one of ${USER_GROUPS.join(", ")}, not ${JSON.stringify(userGroup)}. A rule the build cannot read is a route with no rule at all.`);
115
+ if (!("userGroupIds" in visibility)) return;
116
+ if (userGroup !== "LOGGED_IN_USERS") reject(`userGroupIds only means anything under LOGGED_IN_USERS, not under ${userGroup}`);
117
+ const ids = visibility["userGroupIds"];
118
+ if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) reject(`userGroupIds must be an array of group id strings, not ${JSON.stringify(ids)}. Drop the key entirely to mean "any logged-in user".`);
119
+ }
120
+ function visibilityOf(opts, filePath) {
121
+ const projectRelative = toProjectRelative(opts.root, opts.crawl.routesDirectory, filePath);
122
+ const source = opts.sources.get(path.resolve(opts.root, opts.crawl.routesDirectory, filePath));
123
+ if (source === void 0) return null;
124
+ const result = extractVisibility(source, projectRelative);
125
+ if (!result.ok) throw new ManifestError(result.error);
126
+ if (result.visibility === null) return null;
127
+ const declared = {
128
+ ...result.visibility,
129
+ filePath: projectRelative
130
+ };
131
+ checkVisibilityShape(declared);
132
+ return declared;
133
+ }
134
+ /**
135
+ * Only ENOENT becomes `null`. The caller reads `null` as "nothing declared here", so swallowing any
136
+ * other read error would turn an unreadable declaration into an ungated route.
137
+ */
138
+ async function readIfPresent(filePath) {
139
+ try {
140
+ return await readFile(filePath, "utf8");
141
+ } catch (error) {
142
+ if (error.code === "ENOENT") return null;
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ //#endregion
148
+ export { ManifestError, generateRoutesManifest };
@@ -0,0 +1,71 @@
1
+ import * as nodeFs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ //#region src/routes/mem-fs.ts
5
+ /**
6
+ * Reads through to disk and swallows every write. Driving the generator without this rewrites the
7
+ * app's route sources and strips Start's `Register` block from its routeTree.gen.ts.
8
+ */
9
+ function createReadOnlyFs() {
10
+ const writes = /* @__PURE__ */ new Map();
11
+ return {
12
+ async stat(filePath) {
13
+ const stat = await statOrNull(path.resolve(filePath));
14
+ if (stat === null) return {
15
+ mtimeMs: SYNTHETIC_MTIME,
16
+ mode: 420,
17
+ uid: 0,
18
+ gid: 0
19
+ };
20
+ return {
21
+ mtimeMs: stat.mtimeMs,
22
+ mode: Number(stat.mode),
23
+ uid: Number(stat.uid),
24
+ gid: Number(stat.gid)
25
+ };
26
+ },
27
+ async rename(oldPath, newPath) {
28
+ const from = path.resolve(oldPath);
29
+ const content = writes.get(from);
30
+ if (content !== void 0) {
31
+ writes.delete(from);
32
+ writes.set(path.resolve(newPath), content);
33
+ }
34
+ },
35
+ async writeFile(filePath, content) {
36
+ writes.set(path.resolve(filePath), content);
37
+ },
38
+ async readFile(filePath) {
39
+ const resolved = path.resolve(filePath);
40
+ const pending = writes.get(resolved);
41
+ const stat = await statOrNull(resolved);
42
+ if (pending !== void 0) return {
43
+ stat: { mtimeMs: stat?.mtimeMs ?? SYNTHETIC_MTIME },
44
+ fileContent: pending
45
+ };
46
+ if (stat === null) return "file-not-existing";
47
+ try {
48
+ const fileContent = await nodeFs.readFile(resolved, "utf8");
49
+ return {
50
+ stat: { mtimeMs: stat.mtimeMs },
51
+ fileContent
52
+ };
53
+ } catch {
54
+ return "file-not-existing";
55
+ }
56
+ },
57
+ async chmod() {},
58
+ async chown() {}
59
+ };
60
+ }
61
+ const SYNTHETIC_MTIME = 1n;
62
+ async function statOrNull(filePath) {
63
+ try {
64
+ return await nodeFs.stat(filePath, { bigint: true });
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ //#endregion
71
+ export { createReadOnlyFs };
@@ -0,0 +1,142 @@
1
+ import { SoftrUserGroupId, User } from "../auth/user.js";
2
+ import { SoftrContext } from "../auth/context.js";
3
+
4
+ //#region src/routes/visibility.d.ts
5
+
6
+ /**
7
+ * Who may see a route.
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
+ /**
23
+ * Why a visitor was denied.
24
+ *
25
+ * - `unauthenticated` — the route needs a signed-in visitor and there is none.
26
+ * - `forbidden` — signed in, but in none of the groups the route names.
27
+ * - `alreadySignedIn` — the route is for signed-out visitors only.
28
+ */
29
+ declare const ACCESS_REASONS: readonly ["unauthenticated", "forbidden", "alreadySignedIn"];
30
+ type AccessReason = (typeof ACCESS_REASONS)[number];
31
+ /**
32
+ * What a guard does when it refuses a visitor: redirect to a path, or throw an
33
+ * {@link AccessError} for the route's `errorComponent` to render. A bare string
34
+ * is shorthand for redirecting there.
35
+ */
36
+ type AccessBehaviour = string | {
37
+ strategy: "redirect";
38
+ to: string;
39
+ } | {
40
+ strategy: "throw";
41
+ };
42
+ /** Per-reason overrides of the default {@link AccessBehaviour}. */
43
+ type GuardOverrides = Partial<Record<AccessReason, AccessBehaviour>>;
44
+ /** The result of {@link VisibilityHandle.check}: passed, or denied with a reason. */
45
+ type AccessCheck = {
46
+ ok: true;
47
+ } | {
48
+ ok: false;
49
+ reason: AccessReason;
50
+ };
51
+ /** The one field of {@link SoftrContext} every access check reads. */
52
+ type AccessContext = Pick<SoftrContext, "user">;
53
+ /** Decides whether a visitor passes a visibility rule, without throwing. */
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
+ } ? AccessContext & {
85
+ user: User;
86
+ } : R extends {
87
+ userGroup: "NON_LOGGED_IN_USERS";
88
+ } ? AccessContext & {
89
+ user: null;
90
+ } : AccessContext;
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: AccessContext;
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: AccessContext): void;
118
+ /**
119
+ * Whether the visitor passes the rule. Narrows `context.user` where the rule
120
+ * allows it.
121
+ */
122
+ isAuthorized(context: AccessContext): context is Narrowed<R>;
123
+ /** The decision and, when a visitor is refused, the reason. Throws nothing. */
124
+ check(context: AccessContext): 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
+ export { AccessBehaviour, AccessCheck, AccessError, AccessReason, RouteVisibility, VisibilityHandle, defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError };