@softr/sdk 0.0.12 → 0.0.14
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 +221 -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 +115 -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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region src/assertions.ts
|
|
2
|
+
function requireDefined(value, message) {
|
|
3
|
+
if (value === null || value === void 0) {
|
|
4
|
+
let error = "Unexpected null or undefined value";
|
|
5
|
+
if (message) error += ": " + message;
|
|
6
|
+
throwAtCaller(requireDefined, new Error(error));
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function assert(condition, message) {
|
|
11
|
+
if (!condition) throwAtCaller(assert, new AssertionError(unwrapErrorMessage(message)));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Shorthand for throw new AssertionError("Error message")
|
|
15
|
+
* Useful because it is an expression instead of a statement.
|
|
16
|
+
* Can be assigned to any value, because it never returns.
|
|
17
|
+
*/
|
|
18
|
+
function panic(message) {
|
|
19
|
+
throwAtCaller(panic, new AssertionError(unwrapErrorMessage(message)));
|
|
20
|
+
}
|
|
21
|
+
function todo(message) {
|
|
22
|
+
throwAtCaller(todo, new UnimplementedError(unwrapErrorMessage(message)));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A soft assertion that throws an error in non prod environments, including
|
|
26
|
+
* staging, but logs an error in production mode.
|
|
27
|
+
* This is useful for cases where something unexpected happens, but you can still
|
|
28
|
+
* continue in a slightly broken state.
|
|
29
|
+
* For example, if a block has defined a style control with a hardcoded id and
|
|
30
|
+
* that style control is not found, this is definitely a bug, but it's still
|
|
31
|
+
* possible to go on rendering with a fallback value for this style.
|
|
32
|
+
* It is recommended to liberally use soft assertions so that we fail loudly in
|
|
33
|
+
* staging and local environments.
|
|
34
|
+
*
|
|
35
|
+
* A hard assertion would be more appropriate if we wanted to display some data
|
|
36
|
+
* from an object but that object is null. It's better to throw an assertion
|
|
37
|
+
* error at the place we detect this, instead of continuing and potentially
|
|
38
|
+
* causing an exception in an unrelated part of the code.
|
|
39
|
+
*/
|
|
40
|
+
function softAssert(condition, message) {
|
|
41
|
+
if (!condition) if (process.env.NODE_ENV !== "production") throwAtCaller(softAssert, new SoftAssertionError(unwrapErrorMessage(message)));
|
|
42
|
+
else console.error(`Assertion failed: ${unwrapErrorMessage(message)}`);
|
|
43
|
+
}
|
|
44
|
+
var AssertionError = class extends Error {
|
|
45
|
+
name = "AssertionError";
|
|
46
|
+
};
|
|
47
|
+
var SoftAssertionError = class extends Error {
|
|
48
|
+
name = "SoftAssertionError";
|
|
49
|
+
};
|
|
50
|
+
var UnimplementedError = class extends Error {
|
|
51
|
+
name = "UnimplementedError";
|
|
52
|
+
};
|
|
53
|
+
function unwrapErrorMessage(message) {
|
|
54
|
+
return typeof message === "function" ? message() : message ?? "";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Uses chrome's non-standard Error.captureStackTrace to set the top of the stack
|
|
58
|
+
* trace to start from the caller of the supplied function {@param f}.
|
|
59
|
+
* Useful in assertion functions to make sure that the stack trace shows the spot
|
|
60
|
+
* where the assertion failed, not the place where the assertion was thrown.
|
|
61
|
+
*/
|
|
62
|
+
function throwAtCaller(f, ex) {
|
|
63
|
+
if (Error.captureStackTrace) Error.captureStackTrace(ex, f);
|
|
64
|
+
throw ex;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { assert, panic, requireDefined, softAssert, todo };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { assert } from "../assertions.js";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/config.ts
|
|
4
|
+
const DEFAULT_SESSION_MAX_AGE = 2160 * 60 * 60;
|
|
5
|
+
function sessionConfig() {
|
|
6
|
+
const missing = [];
|
|
7
|
+
const applicationId = process.env.SOFTR_APPLICATION_ID;
|
|
8
|
+
if (!applicationId) missing.push("SOFTR_APPLICATION_ID");
|
|
9
|
+
const sessionSecret = process.env.SOFTR_SESSION_SECRET;
|
|
10
|
+
if (!sessionSecret) missing.push("SOFTR_SESSION_SECRET");
|
|
11
|
+
if (missing.length > 0) throw new Error(`Missing ${missing.join(", ")}.`);
|
|
12
|
+
assert(applicationId && sessionSecret);
|
|
13
|
+
const rawMaxAge = process.env.SOFTR_SESSION_MAX_AGE;
|
|
14
|
+
let sessionMaxAge = DEFAULT_SESSION_MAX_AGE;
|
|
15
|
+
if (rawMaxAge) {
|
|
16
|
+
sessionMaxAge = Number(rawMaxAge);
|
|
17
|
+
if (!Number.isInteger(sessionMaxAge) || sessionMaxAge <= 0) throw new Error(`SOFTR_SESSION_MAX_AGE must be a positive whole number of seconds, got "${rawMaxAge}".`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
applicationId,
|
|
21
|
+
sessionSecret,
|
|
22
|
+
sessionMaxAge
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { sessionConfig };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { User } from "./user.js";
|
|
2
|
+
import { Permissions } from "./permissions.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/context.d.ts
|
|
5
|
+
/** Who the visitor is and what they may do, for one request. */
|
|
6
|
+
type Session = {
|
|
7
|
+
/** The visitor, or `null` when signed out. */
|
|
8
|
+
user: User | null;
|
|
9
|
+
/** What the visitor may do, signed in or not. */
|
|
10
|
+
permissions: Permissions;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The route context the root route publishes — see `getSoftrContext`. The same
|
|
14
|
+
* session, under the name it carries in the public API.
|
|
15
|
+
*/
|
|
16
|
+
type SoftrContext = Session;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { Session, SoftrContext };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { User } from "./user.js";
|
|
2
|
+
import { DatasourcePermissions } from "../types.js";
|
|
3
|
+
import { Permissions } from "./permissions.js";
|
|
4
|
+
import { Session, SoftrContext } from "./context.js";
|
|
5
|
+
import * as _tanstack_react_start0 from "@tanstack/react-start";
|
|
6
|
+
|
|
7
|
+
//#region src/auth/isomorphic.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the Softr visitor and their permissions for every request, so that
|
|
11
|
+
* `getSoftrContext` and the route guards have something to read.
|
|
12
|
+
*
|
|
13
|
+
* Register it once, as a global request middleware in the app's `src/start.ts`.
|
|
14
|
+
* It touches nothing but the session.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* export const startInstance = createStart(() => ({
|
|
18
|
+
* requestMiddleware: [softrAuthMiddleware()],
|
|
19
|
+
* }))
|
|
20
|
+
*/
|
|
21
|
+
declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, {
|
|
22
|
+
user: User | null;
|
|
23
|
+
permissions: Permissions;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the route context of the root route: the visitor and what they may
|
|
27
|
+
* do. It requires {@link softrAuthMiddleware} to be registered.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* export const Route = createRootRouteWithContext<SoftrContext>()({
|
|
31
|
+
* beforeLoad: async () => await getSoftrContext(),
|
|
32
|
+
* })
|
|
33
|
+
*/
|
|
34
|
+
declare const getSoftrContext: _tanstack_react_start0.OptionalFetcher<undefined, undefined, Promise<Session>>;
|
|
35
|
+
/**
|
|
36
|
+
* The context to hand `createRouter`, before any request has resolved one.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* createRouter({ routeTree, context: initialSoftrContext() })
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* createRouter({ routeTree, context: { ...initialSoftrContext(), something: "else" } })
|
|
43
|
+
*/
|
|
44
|
+
declare function initialSoftrContext(): SoftrContext;
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the current user, returning `null` for anonymous visitors.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* function Greeting() {
|
|
50
|
+
* const user = useCurrentUser()
|
|
51
|
+
* return <p>{user ? `Hello, ${user.name}` : "Please sign in"}</p>
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
declare function useCurrentUser(): User | null;
|
|
55
|
+
/**
|
|
56
|
+
* Resolves what the current user may do with one datasource.
|
|
57
|
+
*
|
|
58
|
+
* A rendering hint: every request is authorized again on the server, so never
|
|
59
|
+
* let this stand in for a permission check.
|
|
60
|
+
*
|
|
61
|
+
* Requires the root route to resolve {@link getSoftrContext}.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* function NewRecordButton() {
|
|
65
|
+
* const { create } = usePermissions(datasourceId)
|
|
66
|
+
* return create ? <button>New record</button> : null
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* @throws if the application has no such datasource.
|
|
70
|
+
*/
|
|
71
|
+
declare function usePermissions(datasourceId: string): DatasourcePermissions;
|
|
72
|
+
//#endregion
|
|
73
|
+
export { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { assert, panic, requireDefined } from "../assertions.js";
|
|
2
|
+
import { writeCookies } from "../cookies.js";
|
|
3
|
+
import { sessionConfig } from "./config.js";
|
|
4
|
+
import { makePlatform } from "./platform.js";
|
|
5
|
+
import { clearSession, makeSessionDataCookie, readSessionData, readSessionToken } from "./session.js";
|
|
6
|
+
import { rootRouteId, useRouteContext } from "@tanstack/react-router";
|
|
7
|
+
import { createMiddleware, createServerFn, getGlobalStartContext } from "@tanstack/react-start";
|
|
8
|
+
|
|
9
|
+
//#region src/auth/isomorphic.ts
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the Softr visitor and their permissions for every request, so that
|
|
12
|
+
* `getSoftrContext` and the route guards have something to read.
|
|
13
|
+
*
|
|
14
|
+
* Register it once, as a global request middleware in the app's `src/start.ts`.
|
|
15
|
+
* It touches nothing but the session.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* export const startInstance = createStart(() => ({
|
|
19
|
+
* requestMiddleware: [softrAuthMiddleware()],
|
|
20
|
+
* }))
|
|
21
|
+
*/
|
|
22
|
+
function softrAuthMiddleware() {
|
|
23
|
+
return createMiddleware({ type: "request" }).server(async ({ next }) => {
|
|
24
|
+
async function getSession() {
|
|
25
|
+
const token = readSessionToken();
|
|
26
|
+
const cached = readSessionData(token);
|
|
27
|
+
if (cached !== null) return cached;
|
|
28
|
+
const result = await makePlatform(sessionConfig().applicationId).session(token);
|
|
29
|
+
if (!result.ok) panic(result.message);
|
|
30
|
+
const session = result.data;
|
|
31
|
+
if (token !== null && session.user === null) {
|
|
32
|
+
clearSession();
|
|
33
|
+
return session;
|
|
34
|
+
}
|
|
35
|
+
writeCookies([makeSessionDataCookie(session, token)]);
|
|
36
|
+
return session;
|
|
37
|
+
}
|
|
38
|
+
const { user, permissions } = await getSession();
|
|
39
|
+
return next({ context: {
|
|
40
|
+
user,
|
|
41
|
+
permissions
|
|
42
|
+
} });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the route context of the root route: the visitor and what they may
|
|
47
|
+
* do. It requires {@link softrAuthMiddleware} to be registered.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* export const Route = createRootRouteWithContext<SoftrContext>()({
|
|
51
|
+
* beforeLoad: async () => await getSoftrContext(),
|
|
52
|
+
* })
|
|
53
|
+
*/
|
|
54
|
+
const getSoftrContext = createServerFn({ method: "GET" }).handler(async () => {
|
|
55
|
+
const context = getGlobalStartContext();
|
|
56
|
+
assert(typeof context === "object" && context !== null && "user" in context && "permissions" in context, "softrAuthMiddleware has not run: the Start global context carries no session. Register it as a global request middleware, e.g. createStart(() => ({ requestMiddleware: [softrAuthMiddleware()] })).");
|
|
57
|
+
return context;
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* The context to hand `createRouter`, before any request has resolved one.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* createRouter({ routeTree, context: initialSoftrContext() })
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* createRouter({ routeTree, context: { ...initialSoftrContext(), something: "else" } })
|
|
67
|
+
*/
|
|
68
|
+
function initialSoftrContext() {
|
|
69
|
+
return {
|
|
70
|
+
user: null,
|
|
71
|
+
permissions: { datasource: {} }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolves the current user, returning `null` for anonymous visitors.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* function Greeting() {
|
|
79
|
+
* const user = useCurrentUser()
|
|
80
|
+
* return <p>{user ? `Hello, ${user.name}` : "Please sign in"}</p>
|
|
81
|
+
* }
|
|
82
|
+
*/
|
|
83
|
+
function useCurrentUser() {
|
|
84
|
+
return useRouteContext({
|
|
85
|
+
from: rootRouteId,
|
|
86
|
+
select: (context) => context.user
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolves what the current user may do with one datasource.
|
|
91
|
+
*
|
|
92
|
+
* A rendering hint: every request is authorized again on the server, so never
|
|
93
|
+
* let this stand in for a permission check.
|
|
94
|
+
*
|
|
95
|
+
* Requires the root route to resolve {@link getSoftrContext}.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* function NewRecordButton() {
|
|
99
|
+
* const { create } = usePermissions(datasourceId)
|
|
100
|
+
* return create ? <button>New record</button> : null
|
|
101
|
+
* }
|
|
102
|
+
*
|
|
103
|
+
* @throws if the application has no such datasource.
|
|
104
|
+
*/
|
|
105
|
+
function usePermissions(datasourceId) {
|
|
106
|
+
return useRouteContext({
|
|
107
|
+
from: rootRouteId,
|
|
108
|
+
select: (context) => requireDefined(context.permissions.datasource[datasourceId], `the application has no datasource "${datasourceId}"`)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
export { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { User } from "../user.js";
|
|
2
|
+
import { AuthError, Result } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/methods/password.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Signs a visitor in with an email and password.
|
|
8
|
+
*/
|
|
9
|
+
declare function signIn(credentials: {
|
|
10
|
+
email: string;
|
|
11
|
+
password: string;
|
|
12
|
+
}): Promise<Result<User, AuthError>>;
|
|
13
|
+
/**
|
|
14
|
+
* Signs the visitor out and clears the session cookies.
|
|
15
|
+
*
|
|
16
|
+
* This revokes **every** outstanding session for that user in this app, on every
|
|
17
|
+
* device: the platform stamps `tokensInvalidatedAt` and rejects anything issued
|
|
18
|
+
* before it. There is no per-device sign-out.
|
|
19
|
+
*/
|
|
20
|
+
declare function signOut(): Promise<void>;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { signIn, signOut };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { panic, softAssert, todo } from "../../assertions.js";
|
|
2
|
+
import { writeCookies } from "../../cookies.js";
|
|
3
|
+
import { sessionConfig } from "../config.js";
|
|
4
|
+
import { makePlatform } from "../platform.js";
|
|
5
|
+
import { clearSession, makeSessionDataCookie, makeSessionTokenCookie, readSessionToken } from "../session.js";
|
|
6
|
+
import { authError } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
//#region src/auth/methods/password.ts
|
|
9
|
+
/**
|
|
10
|
+
* Signs a visitor in with an email and password.
|
|
11
|
+
*/
|
|
12
|
+
async function signIn(credentials) {
|
|
13
|
+
const platform = makePlatform(sessionConfig().applicationId);
|
|
14
|
+
const result = await platform.signIn(credentials);
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
if (result.status === 401 || result.status === 429) return authError(result.status === 401 ? "invalidCredentials" : "tooManyAttempts", result.message);
|
|
17
|
+
if (result.status >= 500) panic(result.message);
|
|
18
|
+
if (result.status === 400) todo(result.message);
|
|
19
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows sign-in.`);
|
|
20
|
+
}
|
|
21
|
+
const body = result.data;
|
|
22
|
+
if (body?.authenticatorAppAction) todo(`Sign-in needs an authenticator step (${body.authenticatorAppAction}), which the SDK cannot finish yet.`);
|
|
23
|
+
const token = body?.jwt;
|
|
24
|
+
if (!token) todo("This app is configured for passwordless sign-in, which the SDK cannot finish yet.");
|
|
25
|
+
const sessionResult = await platform.session(token);
|
|
26
|
+
if (!sessionResult.ok) panic(sessionResult.message);
|
|
27
|
+
const session = sessionResult.data;
|
|
28
|
+
if (session.user === null) panic("Session token issued, but no user came back with it.");
|
|
29
|
+
writeCookies([makeSessionTokenCookie(token), makeSessionDataCookie(session, token)]);
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
data: session.user
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Signs the visitor out and clears the session cookies.
|
|
37
|
+
*
|
|
38
|
+
* This revokes **every** outstanding session for that user in this app, on every
|
|
39
|
+
* device: the platform stamps `tokensInvalidatedAt` and rejects anything issued
|
|
40
|
+
* before it. There is no per-device sign-out.
|
|
41
|
+
*/
|
|
42
|
+
async function signOut() {
|
|
43
|
+
const token = readSessionToken();
|
|
44
|
+
clearSession();
|
|
45
|
+
if (!token) return;
|
|
46
|
+
const result = await makePlatform(sessionConfig().applicationId).signOut(token);
|
|
47
|
+
softAssert(result.ok || result.status < 500, () => `Could not revoke the session (${result.status}). This browser is signed out, but other devices may still be.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { signIn, signOut };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { User } from "../user.js";
|
|
2
|
+
import { AuthError, Result } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/methods/signup.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Starts a sign-up flow by emailing a verification code.
|
|
8
|
+
*
|
|
9
|
+
* User only exists once `confirmSignUp` returns the code.
|
|
10
|
+
*/
|
|
11
|
+
declare function signUp(details: {
|
|
12
|
+
email: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
}): Promise<Result<null, AuthError>>;
|
|
15
|
+
/**
|
|
16
|
+
* Finishes sign-up flow with the emailed code and signs the visitor in.
|
|
17
|
+
*
|
|
18
|
+
* The platform splits on how the app is configured: a code-only app creates the
|
|
19
|
+
* user on the second `sign-up` request and answers with a token, while a
|
|
20
|
+
* password app answers it with an empty 200 and expects the password on
|
|
21
|
+
* `users/new`.
|
|
22
|
+
*/
|
|
23
|
+
declare function confirmSignUp(details: {
|
|
24
|
+
email: string;
|
|
25
|
+
/**
|
|
26
|
+
* What `users/new` creates the account with, and unused on a code-only app,
|
|
27
|
+
* which never reaches that request.
|
|
28
|
+
*
|
|
29
|
+
* TODO: temporary in one respect — it is spent a second time on the
|
|
30
|
+
* 400-after-create workaround below, which goes away with the platform fix.
|
|
31
|
+
*/
|
|
32
|
+
password: string;
|
|
33
|
+
code: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
}): Promise<Result<User, AuthError>>;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { confirmSignUp, signUp };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { panic, todo } from "../../assertions.js";
|
|
2
|
+
import { writeCookies } from "../../cookies.js";
|
|
3
|
+
import { sessionConfig } from "../config.js";
|
|
4
|
+
import { makePlatform } from "../platform.js";
|
|
5
|
+
import { makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
|
|
6
|
+
import { authError } from "./utils.js";
|
|
7
|
+
import { signIn } from "./password.js";
|
|
8
|
+
|
|
9
|
+
//#region src/auth/methods/signup.ts
|
|
10
|
+
/**
|
|
11
|
+
* Starts a sign-up flow by emailing a verification code.
|
|
12
|
+
*
|
|
13
|
+
* User only exists once `confirmSignUp` returns the code.
|
|
14
|
+
*/
|
|
15
|
+
async function signUp(details) {
|
|
16
|
+
const result = await makePlatform(sessionConfig().applicationId).signUp(signUpBody(details));
|
|
17
|
+
if (!result.ok) {
|
|
18
|
+
if (result.status >= 500) panic(result.message);
|
|
19
|
+
if (result.status === 409) return authError("emailTaken", result.message);
|
|
20
|
+
return authError("invalidDetails", result.message);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
success: true,
|
|
24
|
+
data: null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finishes sign-up flow with the emailed code and signs the visitor in.
|
|
29
|
+
*
|
|
30
|
+
* The platform splits on how the app is configured: a code-only app creates the
|
|
31
|
+
* user on the second `sign-up` request and answers with a token, while a
|
|
32
|
+
* password app answers it with an empty 200 and expects the password on
|
|
33
|
+
* `users/new`.
|
|
34
|
+
*/
|
|
35
|
+
async function confirmSignUp(details) {
|
|
36
|
+
const platform = makePlatform(sessionConfig().applicationId);
|
|
37
|
+
const verified = await platform.signUp({
|
|
38
|
+
...signUpBody(details),
|
|
39
|
+
token: details.code
|
|
40
|
+
});
|
|
41
|
+
if (!verified.ok) {
|
|
42
|
+
if (verified.status >= 500) panic(verified.message);
|
|
43
|
+
if (verified.status === 409) return authError("emailTaken", verified.message);
|
|
44
|
+
return authError("invalidCode", verified.message);
|
|
45
|
+
}
|
|
46
|
+
let token = verified.data?.jwt;
|
|
47
|
+
if (!token) {
|
|
48
|
+
const created = await platform.new({
|
|
49
|
+
...signUpBody(details),
|
|
50
|
+
password: details.password,
|
|
51
|
+
token: details.code
|
|
52
|
+
});
|
|
53
|
+
if (created.ok) token = created.data?.jwt;
|
|
54
|
+
else {
|
|
55
|
+
if (created.status >= 500) panic(created.message);
|
|
56
|
+
if (created.status === 409) return authError("emailTaken", created.message);
|
|
57
|
+
const signedIn = await signIn({
|
|
58
|
+
email: details.email,
|
|
59
|
+
password: details.password
|
|
60
|
+
});
|
|
61
|
+
if (!signedIn.success) return authError("invalidCode", created.message);
|
|
62
|
+
return signedIn;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!token) todo("The sign-up returned no session token. This app is configured for a sign-up flow the SDK cannot finish yet.");
|
|
66
|
+
const result = await platform.session(token);
|
|
67
|
+
if (!result.ok) panic(result.message);
|
|
68
|
+
const session = result.data;
|
|
69
|
+
if (session.user === null) panic("Session token issued, but no user could be resolved for it.");
|
|
70
|
+
writeCookies([makeSessionTokenCookie(token), makeSessionDataCookie(session, token)]);
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
data: session.user
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `name` is dropped when absent rather than sent as `undefined`: the platform's
|
|
78
|
+
* validator runs on whatever key is present, and an explicit null is not the
|
|
79
|
+
* same request as no name at all.
|
|
80
|
+
*/
|
|
81
|
+
function signUpBody(details) {
|
|
82
|
+
return details.name === void 0 ? { email: details.email } : {
|
|
83
|
+
email: details.email,
|
|
84
|
+
name: details.name
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { confirmSignUp, signUp };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/auth/methods/utils.d.ts
|
|
2
|
+
type Result<T, E = Error> = {
|
|
3
|
+
success: false;
|
|
4
|
+
error: E;
|
|
5
|
+
} | {
|
|
6
|
+
success: true;
|
|
7
|
+
data: T;
|
|
8
|
+
};
|
|
9
|
+
type AuthError = {
|
|
10
|
+
code: AuthErrorCode;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
type AuthErrorCode = "invalidCredentials" | "tooManyAttempts" | "emailTaken" | "invalidDetails" | "invalidCode";
|
|
14
|
+
//#endregion
|
|
15
|
+
export { AuthError, Result };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DatasourcePermissions } from "../types.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/permissions.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What the visitor may do in this app, as the platform sees it.
|
|
8
|
+
*
|
|
9
|
+
* Rendering hints, not enforcement: every data request is authorized again
|
|
10
|
+
* server-side, and these may go stale between the two.
|
|
11
|
+
*/
|
|
12
|
+
type Permissions = {
|
|
13
|
+
/** Keyed by datasource id. */
|
|
14
|
+
datasource: Record<string, DatasourcePermissions>;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { Permissions };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/permissions.ts
|
|
4
|
+
const datasourcePermissionsSchema = z.object({
|
|
5
|
+
read: z.boolean(),
|
|
6
|
+
create: z.boolean(),
|
|
7
|
+
update: z.boolean(),
|
|
8
|
+
delete: z.boolean()
|
|
9
|
+
});
|
|
10
|
+
const permissionsSchema = z.object({ datasource: z.record(z.string(), datasourcePermissionsSchema) });
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { permissionsSchema };
|