@jskit-ai/auth-core 0.1.4
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/package.descriptor.mjs +95 -0
- package/package.json +51 -0
- package/src/client/authApi.js +1 -0
- package/src/client/index.js +2 -0
- package/src/client/providers/AccessCoreClientProvider.js +23 -0
- package/src/client/providers/FastifyAuthPolicyClientProvider.js +13 -0
- package/src/client/signOutFlow.js +1 -0
- package/src/server/inviteTokens.js +41 -0
- package/src/server/lib/actionContextContributor.js +36 -0
- package/src/server/lib/authPolicySupport.js +38 -0
- package/src/server/lib/errors.js +20 -0
- package/src/server/lib/index.js +3 -0
- package/src/server/lib/objectUtils.js +5 -0
- package/src/server/lib/plugin.js +247 -0
- package/src/server/lib/routeMeta.js +64 -0
- package/src/server/lib/routeVisibilityResolver.js +25 -0
- package/src/server/lib/tokens.js +3 -0
- package/src/server/membershipAccess.js +67 -0
- package/src/server/providers/AccessCoreServiceProvider.js +35 -0
- package/src/server/providers/FastifyAuthPolicyServiceProvider.js +124 -0
- package/src/server/utils.js +26 -0
- package/src/server/validators.js +183 -0
- package/src/shared/authApi.js +50 -0
- package/src/shared/authConstraints.js +13 -0
- package/src/shared/authMethods.js +170 -0
- package/src/shared/authPaths.js +24 -0
- package/src/shared/commands/authCommandValidators.js +255 -0
- package/src/shared/commands/authLoginOAuthCompleteCommand.js +68 -0
- package/src/shared/commands/authLoginOAuthStartCommand.js +72 -0
- package/src/shared/commands/authLoginOtpRequestCommand.js +56 -0
- package/src/shared/commands/authLoginOtpVerifyCommand.js +64 -0
- package/src/shared/commands/authLoginPasswordCommand.js +57 -0
- package/src/shared/commands/authLogoutCommand.js +23 -0
- package/src/shared/commands/authPasswordRecoveryCompleteCommand.js +67 -0
- package/src/shared/commands/authPasswordResetCommand.js +49 -0
- package/src/shared/commands/authPasswordResetRequestCommand.js +50 -0
- package/src/shared/commands/authRegisterCommand.js +57 -0
- package/src/shared/commands/authSessionReadCommand.js +26 -0
- package/src/shared/index.js +3 -0
- package/src/shared/inputNormalization.js +1 -0
- package/src/shared/inviteTokens.js +38 -0
- package/src/shared/oauthCallbackParams.js +5 -0
- package/src/shared/oauthProviders.js +66 -0
- package/src/shared/signOutFlow.js +28 -0
- package/test/actionContextContributor.test.js +44 -0
- package/test/authApi.test.js +47 -0
- package/test/authMethods.test.js +95 -0
- package/test/authPaths.test.js +17 -0
- package/test/commandValidators.test.js +33 -0
- package/test/plugin.test.js +250 -0
- package/test/providerRuntime.test.js +114 -0
- package/test/routeMeta.test.js +95 -0
- package/test/routeVisibilityResolver.test.js +34 -0
- package/test/serverUtils.test.js +28 -0
- package/test/signOutFlow.test.js +67 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function resolveMembershipRoleId(membershipLike) {
|
|
2
|
+
return String(membershipLike?.roleId || "").trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function resolveMembershipStatus(membershipLike) {
|
|
6
|
+
return String(membershipLike?.status || membershipLike?.membershipStatus || "active").trim() || "active";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeMembershipForAccess(membershipLike) {
|
|
10
|
+
const roleId = resolveMembershipRoleId(membershipLike);
|
|
11
|
+
if (!roleId) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const status = resolveMembershipStatus(membershipLike);
|
|
16
|
+
if (status !== "active") {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
roleId,
|
|
22
|
+
status
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mapMembershipSummary(membershipLike) {
|
|
27
|
+
return normalizeMembershipForAccess(membershipLike);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePermissions(value) {
|
|
31
|
+
if (!Array.isArray(value)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Array.from(new Set(value.map((permission) => String(permission || "").trim()).filter(Boolean)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createMembershipIndexes(memberships) {
|
|
39
|
+
const byId = new Map();
|
|
40
|
+
const bySlug = new Map();
|
|
41
|
+
|
|
42
|
+
for (const membership of memberships) {
|
|
43
|
+
const workspaceId = Number(membership?.id);
|
|
44
|
+
const workspaceSlug = String(membership?.slug || "").trim();
|
|
45
|
+
|
|
46
|
+
if (Number.isInteger(workspaceId) && workspaceId > 0) {
|
|
47
|
+
byId.set(workspaceId, membership);
|
|
48
|
+
}
|
|
49
|
+
if (workspaceSlug) {
|
|
50
|
+
bySlug.set(workspaceSlug, membership);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
byId,
|
|
56
|
+
bySlug
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
resolveMembershipRoleId,
|
|
62
|
+
resolveMembershipStatus,
|
|
63
|
+
normalizeMembershipForAccess,
|
|
64
|
+
mapMembershipSummary,
|
|
65
|
+
normalizePermissions,
|
|
66
|
+
createMembershipIndexes
|
|
67
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as authConstraints from "../../shared/authConstraints.js";
|
|
2
|
+
import * as authMethods from "../../shared/authMethods.js";
|
|
3
|
+
import * as oauthProviders from "../../shared/oauthProviders.js";
|
|
4
|
+
import * as oauthCallbackParams from "../../shared/oauthCallbackParams.js";
|
|
5
|
+
import * as membershipAccess from "../membershipAccess.js";
|
|
6
|
+
import * as inviteTokens from "../inviteTokens.js";
|
|
7
|
+
import * as utils from "../utils.js";
|
|
8
|
+
import * as validators from "../validators.js";
|
|
9
|
+
|
|
10
|
+
const ACCESS_CORE_API = Object.freeze({
|
|
11
|
+
authConstraints,
|
|
12
|
+
authMethods,
|
|
13
|
+
oauthProviders,
|
|
14
|
+
oauthCallbackParams,
|
|
15
|
+
membershipAccess,
|
|
16
|
+
inviteTokens,
|
|
17
|
+
utils,
|
|
18
|
+
validators
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
class AccessCoreServiceProvider {
|
|
22
|
+
static id = "auth.access";
|
|
23
|
+
|
|
24
|
+
register(app) {
|
|
25
|
+
if (!app || typeof app.singleton !== "function") {
|
|
26
|
+
throw new Error("AccessCoreServiceProvider requires application singleton().");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
app.singleton("auth.access", () => ACCESS_CORE_API);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
boot() {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { AccessCoreServiceProvider };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
2
|
+
import { registerActionContextContributor } from "@jskit-ai/kernel/server/actions";
|
|
3
|
+
import { registerRouteVisibilityResolver } from "@jskit-ai/kernel/server/http";
|
|
4
|
+
import { authPolicyPlugin } from "../lib/plugin.js";
|
|
5
|
+
import { AUTH_POLICY_CONTEXT_RESOLVER_TOKEN } from "../lib/tokens.js";
|
|
6
|
+
import { createAuthActionContextContributor } from "../lib/actionContextContributor.js";
|
|
7
|
+
import { createAuthRouteVisibilityResolver } from "../lib/routeVisibilityResolver.js";
|
|
8
|
+
|
|
9
|
+
const AUTH_ACTION_CONTEXT_CONTRIBUTOR_TOKEN = "auth.policy.actionContextContributor";
|
|
10
|
+
const AUTH_ROUTE_VISIBILITY_RESOLVER_TOKEN = "auth.policy.routeVisibilityResolver";
|
|
11
|
+
|
|
12
|
+
function parseBoolean(value, fallback = false) {
|
|
13
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
14
|
+
if (!raw) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
if (["1", "true", "yes", "on"].includes(raw)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (["0", "false", "no", "off"].includes(raw)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseList(value) {
|
|
27
|
+
return String(value || "")
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((entry) => entry.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function defaultHasPermission({ permission, permissions = [] } = {}) {
|
|
34
|
+
if (!permission) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return Array.isArray(permissions) ? permissions.includes(permission) : false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class FastifyAuthPolicyServiceProvider {
|
|
41
|
+
static id = "auth.policy.fastify";
|
|
42
|
+
|
|
43
|
+
static dependsOn = ["auth.provider"];
|
|
44
|
+
|
|
45
|
+
register(app) {
|
|
46
|
+
if (!app || typeof app.has !== "function") {
|
|
47
|
+
throw new Error("FastifyAuthPolicyServiceProvider requires application has().");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
!app.has(AUTH_ACTION_CONTEXT_CONTRIBUTOR_TOKEN) &&
|
|
52
|
+
typeof app.singleton === "function" &&
|
|
53
|
+
typeof app.tag === "function"
|
|
54
|
+
) {
|
|
55
|
+
registerActionContextContributor(app, AUTH_ACTION_CONTEXT_CONTRIBUTOR_TOKEN, () =>
|
|
56
|
+
createAuthActionContextContributor()
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
!app.has(AUTH_ROUTE_VISIBILITY_RESOLVER_TOKEN) &&
|
|
62
|
+
typeof app.singleton === "function" &&
|
|
63
|
+
typeof app.tag === "function"
|
|
64
|
+
) {
|
|
65
|
+
registerRouteVisibilityResolver(app, AUTH_ROUTE_VISIBILITY_RESOLVER_TOKEN, () =>
|
|
66
|
+
createAuthRouteVisibilityResolver()
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async boot(app) {
|
|
72
|
+
if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
|
|
73
|
+
throw new Error("FastifyAuthPolicyServiceProvider requires application make()/has().");
|
|
74
|
+
}
|
|
75
|
+
if (!app.has("authService")) {
|
|
76
|
+
throw new Error("FastifyAuthPolicyServiceProvider requires authService binding.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const env = app.has(KERNEL_TOKENS.Env) ? app.make(KERNEL_TOKENS.Env) : {};
|
|
80
|
+
const fastify = app.make(KERNEL_TOKENS.Fastify);
|
|
81
|
+
const authService = app.make("authService");
|
|
82
|
+
const resolveContext =
|
|
83
|
+
typeof app.has === "function" && app.has(AUTH_POLICY_CONTEXT_RESOLVER_TOKEN)
|
|
84
|
+
? app.make(AUTH_POLICY_CONTEXT_RESOLVER_TOKEN)
|
|
85
|
+
: null;
|
|
86
|
+
|
|
87
|
+
if (resolveContext != null && typeof resolveContext !== "function") {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`FastifyAuthPolicyServiceProvider requires ${AUTH_POLICY_CONTEXT_RESOLVER_TOKEN} to be a function when provided.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pluginDeps = {
|
|
94
|
+
resolveActor: async (request) => {
|
|
95
|
+
if (authService && typeof authService.authenticateRequest === "function") {
|
|
96
|
+
return authService.authenticateRequest(request);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
authenticated: false,
|
|
100
|
+
actor: null,
|
|
101
|
+
transientFailure: false
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
hasPermission: defaultHasPermission,
|
|
105
|
+
...(typeof resolveContext === "function" ? { resolveContext } : {})
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const plugin = authPolicyPlugin(
|
|
109
|
+
pluginDeps,
|
|
110
|
+
{
|
|
111
|
+
nodeEnv: String(env.NODE_ENV || "development").trim() || "development",
|
|
112
|
+
apiPrefix: String(env.AUTH_API_PREFIX || "/api/").trim() || "/api/",
|
|
113
|
+
unsafeMethods: parseList(env.AUTH_CSRF_UNSAFE_METHODS),
|
|
114
|
+
csrfCookieOpts: {
|
|
115
|
+
secure: parseBoolean(env.AUTH_CSRF_COOKIE_SECURE, false)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await plugin(fastify);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { FastifyAuthPolicyServiceProvider };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeReturnToPath as normalizeSharedReturnToPath } from "@jskit-ai/kernel/shared/support";
|
|
2
|
+
|
|
3
|
+
export function normalizeEmail(value) {
|
|
4
|
+
return String(value || "")
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeOAuthIntent(value, { fallback = "login" } = {}) {
|
|
10
|
+
const normalized = String(value || "")
|
|
11
|
+
.trim()
|
|
12
|
+
.toLowerCase();
|
|
13
|
+
|
|
14
|
+
if (normalized === "login" || normalized === "link") {
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeReturnToPath(value, { fallback = "/", allowedOrigins = [] } = {}) {
|
|
22
|
+
return normalizeSharedReturnToPath(value, {
|
|
23
|
+
fallback,
|
|
24
|
+
allowedOrigins
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTH_EMAIL_MAX_LENGTH,
|
|
3
|
+
AUTH_EMAIL_REGEX,
|
|
4
|
+
AUTH_LOGIN_PASSWORD_MAX_LENGTH,
|
|
5
|
+
AUTH_PASSWORD_MAX_LENGTH,
|
|
6
|
+
AUTH_PASSWORD_MIN_LENGTH
|
|
7
|
+
} from "../shared/authConstraints.js";
|
|
8
|
+
import { normalizeEmail } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
function validateEmail(rawEmail) {
|
|
11
|
+
const email = normalizeEmail(rawEmail);
|
|
12
|
+
|
|
13
|
+
if (!email) {
|
|
14
|
+
return {
|
|
15
|
+
email,
|
|
16
|
+
error: "Email is required."
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (email.length > AUTH_EMAIL_MAX_LENGTH || !AUTH_EMAIL_REGEX.test(email)) {
|
|
21
|
+
return {
|
|
22
|
+
email,
|
|
23
|
+
error: "Provide a valid email address."
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
email,
|
|
29
|
+
error: ""
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function registerPassword(rawPassword) {
|
|
34
|
+
const password = String(rawPassword || "");
|
|
35
|
+
if (!password) {
|
|
36
|
+
return {
|
|
37
|
+
password,
|
|
38
|
+
error: "Password is required."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (password.length < AUTH_PASSWORD_MIN_LENGTH || password.length > AUTH_PASSWORD_MAX_LENGTH) {
|
|
43
|
+
return {
|
|
44
|
+
password,
|
|
45
|
+
error: "Password must be between 8 and 128 characters."
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
password,
|
|
51
|
+
error: ""
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loginPassword(rawPassword) {
|
|
56
|
+
const password = String(rawPassword || "");
|
|
57
|
+
if (!password) {
|
|
58
|
+
return {
|
|
59
|
+
password,
|
|
60
|
+
error: "Password is required."
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (password.length > AUTH_LOGIN_PASSWORD_MAX_LENGTH) {
|
|
65
|
+
return {
|
|
66
|
+
password,
|
|
67
|
+
error: `Password must be at most ${AUTH_LOGIN_PASSWORD_MAX_LENGTH} characters.`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
password,
|
|
73
|
+
error: ""
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function confirmPassword(rawPassword, rawConfirmPassword) {
|
|
78
|
+
const password = String(rawPassword || "");
|
|
79
|
+
const confirmPassword = String(rawConfirmPassword || "");
|
|
80
|
+
|
|
81
|
+
if (!confirmPassword) {
|
|
82
|
+
return "Confirm your password.";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (password !== confirmPassword) {
|
|
86
|
+
return "Passwords do not match.";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resetPassword(rawPassword) {
|
|
93
|
+
return registerPassword(rawPassword);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function registerInput(payload = {}) {
|
|
97
|
+
const emailCheck = validateEmail(payload.email);
|
|
98
|
+
const passwordCheck = registerPassword(payload.password);
|
|
99
|
+
const fieldErrors = {};
|
|
100
|
+
|
|
101
|
+
if (emailCheck.error) {
|
|
102
|
+
fieldErrors.email = emailCheck.error;
|
|
103
|
+
}
|
|
104
|
+
if (passwordCheck.error) {
|
|
105
|
+
fieldErrors.password = passwordCheck.error;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
email: emailCheck.email,
|
|
110
|
+
password: passwordCheck.password,
|
|
111
|
+
fieldErrors
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loginInput(payload = {}) {
|
|
116
|
+
const emailCheck = validateEmail(payload.email);
|
|
117
|
+
const passwordCheck = loginPassword(payload.password);
|
|
118
|
+
const fieldErrors = {};
|
|
119
|
+
|
|
120
|
+
if (emailCheck.error) {
|
|
121
|
+
fieldErrors.email = emailCheck.error;
|
|
122
|
+
}
|
|
123
|
+
if (passwordCheck.error) {
|
|
124
|
+
fieldErrors.password = passwordCheck.error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
email: emailCheck.email,
|
|
129
|
+
password: passwordCheck.password,
|
|
130
|
+
fieldErrors
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function forgotPasswordInput(payload = {}) {
|
|
135
|
+
const emailCheck = validateEmail(payload.email);
|
|
136
|
+
const fieldErrors = {};
|
|
137
|
+
|
|
138
|
+
if (emailCheck.error) {
|
|
139
|
+
fieldErrors.email = emailCheck.error;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
email: emailCheck.email,
|
|
144
|
+
fieldErrors
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resetPasswordInput(payload = {}) {
|
|
149
|
+
const passwordCheck = resetPassword(payload.password);
|
|
150
|
+
const fieldErrors = {};
|
|
151
|
+
|
|
152
|
+
if (passwordCheck.error) {
|
|
153
|
+
fieldErrors.password = passwordCheck.error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
password: passwordCheck.password,
|
|
158
|
+
fieldErrors
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const validators = {
|
|
163
|
+
email: (rawEmail) => validateEmail(rawEmail).error,
|
|
164
|
+
registerPassword: (rawPassword) => registerPassword(rawPassword).error,
|
|
165
|
+
loginPassword: (rawPassword) => loginPassword(rawPassword).error,
|
|
166
|
+
resetPassword: (rawPassword) => resetPassword(rawPassword).error,
|
|
167
|
+
confirmPassword: ({ password, confirmPassword: confirmPasswordValue }) => confirmPassword(password, confirmPasswordValue),
|
|
168
|
+
registerInput,
|
|
169
|
+
loginInput,
|
|
170
|
+
forgotPasswordInput,
|
|
171
|
+
resetPasswordInput
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
confirmPassword,
|
|
176
|
+
forgotPasswordInput,
|
|
177
|
+
loginInput,
|
|
178
|
+
loginPassword,
|
|
179
|
+
registerInput,
|
|
180
|
+
registerPassword,
|
|
181
|
+
resetPassword,
|
|
182
|
+
resetPasswordInput
|
|
183
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AUTH_PATHS, buildAuthOauthStartPath } from "./authPaths.js";
|
|
2
|
+
|
|
3
|
+
function createApi({ request }) {
|
|
4
|
+
return {
|
|
5
|
+
session() {
|
|
6
|
+
return request(AUTH_PATHS.SESSION);
|
|
7
|
+
},
|
|
8
|
+
register(payload) {
|
|
9
|
+
return request(AUTH_PATHS.REGISTER, { method: "POST", body: payload });
|
|
10
|
+
},
|
|
11
|
+
login(payload) {
|
|
12
|
+
return request(AUTH_PATHS.LOGIN, { method: "POST", body: payload });
|
|
13
|
+
},
|
|
14
|
+
requestOtp(payload) {
|
|
15
|
+
return request(AUTH_PATHS.LOGIN_OTP_REQUEST, { method: "POST", body: payload });
|
|
16
|
+
},
|
|
17
|
+
verifyOtp(payload) {
|
|
18
|
+
return request(AUTH_PATHS.LOGIN_OTP_VERIFY, { method: "POST", body: payload });
|
|
19
|
+
},
|
|
20
|
+
oauthStartUrl(provider, options = {}) {
|
|
21
|
+
const oauthStartPath = buildAuthOauthStartPath(provider);
|
|
22
|
+
const returnTo = String(options.returnTo || "").trim();
|
|
23
|
+
if (!returnTo) {
|
|
24
|
+
return oauthStartPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
returnTo
|
|
29
|
+
});
|
|
30
|
+
return `${oauthStartPath}?${params.toString()}`;
|
|
31
|
+
},
|
|
32
|
+
oauthComplete(payload) {
|
|
33
|
+
return request(AUTH_PATHS.OAUTH_COMPLETE, { method: "POST", body: payload });
|
|
34
|
+
},
|
|
35
|
+
requestPasswordReset(payload) {
|
|
36
|
+
return request(AUTH_PATHS.PASSWORD_FORGOT, { method: "POST", body: payload });
|
|
37
|
+
},
|
|
38
|
+
completePasswordRecovery(payload) {
|
|
39
|
+
return request(AUTH_PATHS.PASSWORD_RECOVERY, { method: "POST", body: payload });
|
|
40
|
+
},
|
|
41
|
+
resetPassword(payload) {
|
|
42
|
+
return request(AUTH_PATHS.PASSWORD_RESET, { method: "POST", body: payload });
|
|
43
|
+
},
|
|
44
|
+
logout() {
|
|
45
|
+
return request(AUTH_PATHS.LOGOUT, { method: "POST" });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { createApi };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const AUTH_EMAIL_PATTERN = "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$";
|
|
2
|
+
export const AUTH_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3
|
+
|
|
4
|
+
export const AUTH_EMAIL_MIN_LENGTH = 3;
|
|
5
|
+
export const AUTH_EMAIL_MAX_LENGTH = 320;
|
|
6
|
+
|
|
7
|
+
export const AUTH_PASSWORD_MIN_LENGTH = 8;
|
|
8
|
+
export const AUTH_PASSWORD_MAX_LENGTH = 128;
|
|
9
|
+
export const AUTH_LOGIN_PASSWORD_MAX_LENGTH = 1024;
|
|
10
|
+
|
|
11
|
+
export const AUTH_RECOVERY_TOKEN_MAX_LENGTH = 4096;
|
|
12
|
+
export const AUTH_ACCESS_TOKEN_MAX_LENGTH = 8192;
|
|
13
|
+
export const AUTH_REFRESH_TOKEN_MAX_LENGTH = 8192;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { normalizeOAuthProviderId } from "./oauthProviders.js";
|
|
2
|
+
|
|
3
|
+
const AUTH_METHOD_PASSWORD_ID = "password";
|
|
4
|
+
const AUTH_METHOD_PASSWORD_PROVIDER = "email";
|
|
5
|
+
const AUTH_METHOD_EMAIL_OTP_ID = "email_otp";
|
|
6
|
+
const AUTH_METHOD_EMAIL_OTP_PROVIDER = "email";
|
|
7
|
+
|
|
8
|
+
const AUTH_METHOD_KIND_PASSWORD = "password";
|
|
9
|
+
const AUTH_METHOD_KIND_OTP = "otp";
|
|
10
|
+
const AUTH_METHOD_KIND_OAUTH = "oauth";
|
|
11
|
+
const AUTH_METHOD_KINDS = Object.freeze([AUTH_METHOD_KIND_PASSWORD, AUTH_METHOD_KIND_OTP, AUTH_METHOD_KIND_OAUTH]);
|
|
12
|
+
|
|
13
|
+
const AUTH_METHOD_MINIMUM_ENABLED = 1;
|
|
14
|
+
|
|
15
|
+
const AUTH_METHOD_DEFINITIONS = Object.freeze([
|
|
16
|
+
Object.freeze({
|
|
17
|
+
id: AUTH_METHOD_PASSWORD_ID,
|
|
18
|
+
kind: AUTH_METHOD_KIND_PASSWORD,
|
|
19
|
+
provider: AUTH_METHOD_PASSWORD_PROVIDER,
|
|
20
|
+
label: "Password",
|
|
21
|
+
supportsSecretUpdate: true
|
|
22
|
+
}),
|
|
23
|
+
Object.freeze({
|
|
24
|
+
id: AUTH_METHOD_EMAIL_OTP_ID,
|
|
25
|
+
kind: AUTH_METHOD_KIND_OTP,
|
|
26
|
+
provider: AUTH_METHOD_EMAIL_OTP_PROVIDER,
|
|
27
|
+
label: "Email one-time code",
|
|
28
|
+
supportsSecretUpdate: false
|
|
29
|
+
})
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const AUTH_METHOD_IDS = Object.freeze(AUTH_METHOD_DEFINITIONS.map((definition) => definition.id));
|
|
33
|
+
|
|
34
|
+
function buildOAuthMethodId(providerId) {
|
|
35
|
+
const normalizedProviderId = normalizeOAuthProviderId(providerId, { fallback: null });
|
|
36
|
+
if (!normalizedProviderId) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `oauth:${normalizedProviderId}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseAuthMethodId(value) {
|
|
44
|
+
const normalized = String(value || "")
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
|
|
48
|
+
if (normalized === AUTH_METHOD_PASSWORD_ID) {
|
|
49
|
+
return {
|
|
50
|
+
id: AUTH_METHOD_PASSWORD_ID,
|
|
51
|
+
kind: AUTH_METHOD_KIND_PASSWORD,
|
|
52
|
+
provider: AUTH_METHOD_PASSWORD_PROVIDER
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (normalized === AUTH_METHOD_EMAIL_OTP_ID) {
|
|
57
|
+
return {
|
|
58
|
+
id: AUTH_METHOD_EMAIL_OTP_ID,
|
|
59
|
+
kind: AUTH_METHOD_KIND_OTP,
|
|
60
|
+
provider: AUTH_METHOD_EMAIL_OTP_PROVIDER
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (normalized.startsWith("oauth:")) {
|
|
65
|
+
const providerId = normalizeOAuthProviderId(normalized.slice("oauth:".length), {
|
|
66
|
+
fallback: null
|
|
67
|
+
});
|
|
68
|
+
if (!providerId) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: buildOAuthMethodId(providerId),
|
|
74
|
+
kind: AUTH_METHOD_KIND_OAUTH,
|
|
75
|
+
provider: providerId
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeOAuthMethodDefinitionInput(entry) {
|
|
83
|
+
if (typeof entry === "string") {
|
|
84
|
+
const providerId = normalizeOAuthProviderId(entry, { fallback: null });
|
|
85
|
+
if (!providerId) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
id: providerId,
|
|
91
|
+
label: providerId
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!entry || typeof entry !== "object") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const providerId = normalizeOAuthProviderId(entry.id, { fallback: null });
|
|
100
|
+
if (!providerId) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const label = String(entry.label || providerId).trim() || providerId;
|
|
105
|
+
return {
|
|
106
|
+
id: providerId,
|
|
107
|
+
label
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildOAuthMethodDefinitions(oauthProviders = []) {
|
|
112
|
+
const definitions = [];
|
|
113
|
+
|
|
114
|
+
for (const rawProvider of Array.isArray(oauthProviders) ? oauthProviders : []) {
|
|
115
|
+
const normalized = normalizeOAuthMethodDefinitionInput(rawProvider);
|
|
116
|
+
if (!normalized || definitions.some((definition) => definition.provider === normalized.id)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
definitions.push(
|
|
121
|
+
Object.freeze({
|
|
122
|
+
id: buildOAuthMethodId(normalized.id),
|
|
123
|
+
kind: AUTH_METHOD_KIND_OAUTH,
|
|
124
|
+
provider: normalized.id,
|
|
125
|
+
label: normalized.label,
|
|
126
|
+
supportsSecretUpdate: false
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Object.freeze(definitions);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildAuthMethodDefinitions({ oauthProviders = [] } = {}) {
|
|
135
|
+
return Object.freeze([...AUTH_METHOD_DEFINITIONS, ...buildOAuthMethodDefinitions(oauthProviders)]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildAuthMethodIds(options = {}) {
|
|
139
|
+
return Object.freeze(buildAuthMethodDefinitions(options).map((definition) => definition.id));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findAuthMethodDefinition(methodId, options = {}) {
|
|
143
|
+
const normalized = parseAuthMethodId(methodId);
|
|
144
|
+
if (!normalized) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const found = buildAuthMethodDefinitions(options).find((definition) => definition.id === normalized.id);
|
|
149
|
+
return found || null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
AUTH_METHOD_PASSWORD_ID,
|
|
154
|
+
AUTH_METHOD_PASSWORD_PROVIDER,
|
|
155
|
+
AUTH_METHOD_EMAIL_OTP_ID,
|
|
156
|
+
AUTH_METHOD_EMAIL_OTP_PROVIDER,
|
|
157
|
+
AUTH_METHOD_KIND_PASSWORD,
|
|
158
|
+
AUTH_METHOD_KIND_OTP,
|
|
159
|
+
AUTH_METHOD_KIND_OAUTH,
|
|
160
|
+
AUTH_METHOD_KINDS,
|
|
161
|
+
AUTH_METHOD_MINIMUM_ENABLED,
|
|
162
|
+
AUTH_METHOD_DEFINITIONS,
|
|
163
|
+
AUTH_METHOD_IDS,
|
|
164
|
+
buildOAuthMethodId,
|
|
165
|
+
parseAuthMethodId,
|
|
166
|
+
buildOAuthMethodDefinitions,
|
|
167
|
+
buildAuthMethodDefinitions,
|
|
168
|
+
buildAuthMethodIds,
|
|
169
|
+
findAuthMethodDefinition
|
|
170
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const AUTH_PATHS = Object.freeze({
|
|
2
|
+
REGISTER: "/api/register",
|
|
3
|
+
LOGIN: "/api/login",
|
|
4
|
+
LOGIN_OTP_REQUEST: "/api/login/otp/request",
|
|
5
|
+
LOGIN_OTP_VERIFY: "/api/login/otp/verify",
|
|
6
|
+
OAUTH_START_TEMPLATE: "/api/oauth/:provider/start",
|
|
7
|
+
OAUTH_COMPLETE: "/api/oauth/complete",
|
|
8
|
+
PASSWORD_FORGOT: "/api/password/forgot",
|
|
9
|
+
PASSWORD_RECOVERY: "/api/password/recovery",
|
|
10
|
+
PASSWORD_RESET: "/api/password/reset",
|
|
11
|
+
LOGOUT: "/api/logout",
|
|
12
|
+
SESSION: "/api/session"
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function buildAuthOauthStartPath(provider) {
|
|
16
|
+
const providerId = encodeURIComponent(
|
|
17
|
+
String(provider || "")
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
);
|
|
21
|
+
return AUTH_PATHS.OAUTH_START_TEMPLATE.replace(":provider", providerId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { AUTH_PATHS, buildAuthOauthStartPath };
|