@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.
Files changed (55) hide show
  1. package/package.descriptor.mjs +95 -0
  2. package/package.json +51 -0
  3. package/src/client/authApi.js +1 -0
  4. package/src/client/index.js +2 -0
  5. package/src/client/providers/AccessCoreClientProvider.js +23 -0
  6. package/src/client/providers/FastifyAuthPolicyClientProvider.js +13 -0
  7. package/src/client/signOutFlow.js +1 -0
  8. package/src/server/inviteTokens.js +41 -0
  9. package/src/server/lib/actionContextContributor.js +36 -0
  10. package/src/server/lib/authPolicySupport.js +38 -0
  11. package/src/server/lib/errors.js +20 -0
  12. package/src/server/lib/index.js +3 -0
  13. package/src/server/lib/objectUtils.js +5 -0
  14. package/src/server/lib/plugin.js +247 -0
  15. package/src/server/lib/routeMeta.js +64 -0
  16. package/src/server/lib/routeVisibilityResolver.js +25 -0
  17. package/src/server/lib/tokens.js +3 -0
  18. package/src/server/membershipAccess.js +67 -0
  19. package/src/server/providers/AccessCoreServiceProvider.js +35 -0
  20. package/src/server/providers/FastifyAuthPolicyServiceProvider.js +124 -0
  21. package/src/server/utils.js +26 -0
  22. package/src/server/validators.js +183 -0
  23. package/src/shared/authApi.js +50 -0
  24. package/src/shared/authConstraints.js +13 -0
  25. package/src/shared/authMethods.js +170 -0
  26. package/src/shared/authPaths.js +24 -0
  27. package/src/shared/commands/authCommandValidators.js +255 -0
  28. package/src/shared/commands/authLoginOAuthCompleteCommand.js +68 -0
  29. package/src/shared/commands/authLoginOAuthStartCommand.js +72 -0
  30. package/src/shared/commands/authLoginOtpRequestCommand.js +56 -0
  31. package/src/shared/commands/authLoginOtpVerifyCommand.js +64 -0
  32. package/src/shared/commands/authLoginPasswordCommand.js +57 -0
  33. package/src/shared/commands/authLogoutCommand.js +23 -0
  34. package/src/shared/commands/authPasswordRecoveryCompleteCommand.js +67 -0
  35. package/src/shared/commands/authPasswordResetCommand.js +49 -0
  36. package/src/shared/commands/authPasswordResetRequestCommand.js +50 -0
  37. package/src/shared/commands/authRegisterCommand.js +57 -0
  38. package/src/shared/commands/authSessionReadCommand.js +26 -0
  39. package/src/shared/index.js +3 -0
  40. package/src/shared/inputNormalization.js +1 -0
  41. package/src/shared/inviteTokens.js +38 -0
  42. package/src/shared/oauthCallbackParams.js +5 -0
  43. package/src/shared/oauthProviders.js +66 -0
  44. package/src/shared/signOutFlow.js +28 -0
  45. package/test/actionContextContributor.test.js +44 -0
  46. package/test/authApi.test.js +47 -0
  47. package/test/authMethods.test.js +95 -0
  48. package/test/authPaths.test.js +17 -0
  49. package/test/commandValidators.test.js +33 -0
  50. package/test/plugin.test.js +250 -0
  51. package/test/providerRuntime.test.js +114 -0
  52. package/test/routeMeta.test.js +95 -0
  53. package/test/routeVisibilityResolver.test.js +34 -0
  54. package/test/serverUtils.test.js +28 -0
  55. package/test/signOutFlow.test.js +67 -0
@@ -0,0 +1,3 @@
1
+ const AUTH_POLICY_CONTEXT_RESOLVER_TOKEN = "auth.policy.contextResolver";
2
+
3
+ export { AUTH_POLICY_CONTEXT_RESOLVER_TOKEN };
@@ -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 };