@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,49 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "../inputNormalization.js";
|
|
3
|
+
import {
|
|
4
|
+
authPasswordValidator,
|
|
5
|
+
createCommandMessages,
|
|
6
|
+
okMessageResponseValidator
|
|
7
|
+
} from "./authCommandValidators.js";
|
|
8
|
+
|
|
9
|
+
const AUTH_PASSWORD_RESET_MESSAGES = createCommandMessages({
|
|
10
|
+
fields: {
|
|
11
|
+
password: {
|
|
12
|
+
required: "Password is required.",
|
|
13
|
+
minLength: "Password must be at least 8 characters.",
|
|
14
|
+
default: "Password must be at least 8 characters."
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const authPasswordResetBodyValidator = Object.freeze({
|
|
20
|
+
schema: Type.Object(
|
|
21
|
+
{
|
|
22
|
+
password: authPasswordValidator.schema
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
}
|
|
27
|
+
),
|
|
28
|
+
normalize: normalizeObjectInput,
|
|
29
|
+
messages: AUTH_PASSWORD_RESET_MESSAGES
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const authPasswordResetCommand = Object.freeze({
|
|
33
|
+
command: "auth.password.reset",
|
|
34
|
+
operation: Object.freeze({
|
|
35
|
+
method: "POST",
|
|
36
|
+
bodyValidator: authPasswordResetBodyValidator,
|
|
37
|
+
responseValidator: okMessageResponseValidator,
|
|
38
|
+
messages: AUTH_PASSWORD_RESET_MESSAGES,
|
|
39
|
+
idempotent: false,
|
|
40
|
+
invalidates: Object.freeze(["auth.session.read"])
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
authPasswordResetBodyValidator,
|
|
46
|
+
okMessageResponseValidator,
|
|
47
|
+
AUTH_PASSWORD_RESET_MESSAGES,
|
|
48
|
+
authPasswordResetCommand
|
|
49
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "../inputNormalization.js";
|
|
3
|
+
import {
|
|
4
|
+
authEmailValidator,
|
|
5
|
+
createCommandMessages,
|
|
6
|
+
okMessageResponseValidator
|
|
7
|
+
} from "./authCommandValidators.js";
|
|
8
|
+
|
|
9
|
+
const AUTH_PASSWORD_RESET_REQUEST_MESSAGES = createCommandMessages({
|
|
10
|
+
fields: {
|
|
11
|
+
email: {
|
|
12
|
+
required: "Email is required.",
|
|
13
|
+
minLength: "Email is required.",
|
|
14
|
+
pattern: "Enter a valid email address.",
|
|
15
|
+
default: "Enter a valid email address."
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const authPasswordResetRequestBodyValidator = Object.freeze({
|
|
21
|
+
schema: Type.Object(
|
|
22
|
+
{
|
|
23
|
+
email: authEmailValidator.schema
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
additionalProperties: false
|
|
27
|
+
}
|
|
28
|
+
),
|
|
29
|
+
normalize: normalizeObjectInput,
|
|
30
|
+
messages: AUTH_PASSWORD_RESET_REQUEST_MESSAGES
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const authPasswordResetRequestCommand = Object.freeze({
|
|
34
|
+
command: "auth.password.reset.request",
|
|
35
|
+
operation: Object.freeze({
|
|
36
|
+
method: "POST",
|
|
37
|
+
bodyValidator: authPasswordResetRequestBodyValidator,
|
|
38
|
+
responseValidator: okMessageResponseValidator,
|
|
39
|
+
messages: AUTH_PASSWORD_RESET_REQUEST_MESSAGES,
|
|
40
|
+
idempotent: false,
|
|
41
|
+
invalidates: Object.freeze([])
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
authPasswordResetRequestBodyValidator,
|
|
47
|
+
okMessageResponseValidator,
|
|
48
|
+
AUTH_PASSWORD_RESET_REQUEST_MESSAGES,
|
|
49
|
+
authPasswordResetRequestCommand
|
|
50
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "../inputNormalization.js";
|
|
3
|
+
import {
|
|
4
|
+
authEmailValidator,
|
|
5
|
+
authPasswordValidator,
|
|
6
|
+
createCommandMessages,
|
|
7
|
+
registerResponseValidator
|
|
8
|
+
} from "./authCommandValidators.js";
|
|
9
|
+
|
|
10
|
+
const AUTH_REGISTER_MESSAGES = createCommandMessages({
|
|
11
|
+
fields: {
|
|
12
|
+
email: {
|
|
13
|
+
required: "Email is required.",
|
|
14
|
+
minLength: "Email is required.",
|
|
15
|
+
pattern: "Enter a valid email address.",
|
|
16
|
+
default: "Enter a valid email address."
|
|
17
|
+
},
|
|
18
|
+
password: {
|
|
19
|
+
required: "Password is required.",
|
|
20
|
+
minLength: "Password must be at least 8 characters.",
|
|
21
|
+
default: "Password must be at least 8 characters."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const authRegisterBodyValidator = Object.freeze({
|
|
27
|
+
schema: Type.Object(
|
|
28
|
+
{
|
|
29
|
+
email: authEmailValidator.schema,
|
|
30
|
+
password: authPasswordValidator.schema
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
additionalProperties: false
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
normalize: normalizeObjectInput,
|
|
37
|
+
messages: AUTH_REGISTER_MESSAGES
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const authRegisterCommand = Object.freeze({
|
|
41
|
+
command: "auth.register",
|
|
42
|
+
operation: Object.freeze({
|
|
43
|
+
method: "POST",
|
|
44
|
+
bodyValidator: authRegisterBodyValidator,
|
|
45
|
+
responseValidator: registerResponseValidator,
|
|
46
|
+
messages: AUTH_REGISTER_MESSAGES,
|
|
47
|
+
idempotent: false,
|
|
48
|
+
invalidates: Object.freeze(["auth.session.read"])
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
authRegisterBodyValidator,
|
|
54
|
+
registerResponseValidator,
|
|
55
|
+
AUTH_REGISTER_MESSAGES,
|
|
56
|
+
authRegisterCommand
|
|
57
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCommandMessages,
|
|
3
|
+
sessionResponseValidator,
|
|
4
|
+
sessionUnavailableResponseValidator
|
|
5
|
+
} from "./authCommandValidators.js";
|
|
6
|
+
|
|
7
|
+
const AUTH_SESSION_READ_MESSAGES = createCommandMessages();
|
|
8
|
+
|
|
9
|
+
const authSessionReadCommand = Object.freeze({
|
|
10
|
+
command: "auth.session.read",
|
|
11
|
+
operation: Object.freeze({
|
|
12
|
+
method: "GET",
|
|
13
|
+
responseValidator: sessionResponseValidator,
|
|
14
|
+
unavailableResponseValidator: sessionUnavailableResponseValidator,
|
|
15
|
+
messages: AUTH_SESSION_READ_MESSAGES,
|
|
16
|
+
idempotent: true,
|
|
17
|
+
invalidates: Object.freeze([])
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
sessionResponseValidator,
|
|
23
|
+
sessionUnavailableResponseValidator,
|
|
24
|
+
AUTH_SESSION_READ_MESSAGES,
|
|
25
|
+
authSessionReadCommand
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const OPAQUE_INVITE_TOKEN_HASH_PREFIX = "inviteh_";
|
|
2
|
+
|
|
3
|
+
function normalizeInviteToken(token) {
|
|
4
|
+
return String(token || "").trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isSha256Hex(value) {
|
|
8
|
+
return /^[a-f0-9]{64}$/i.test(String(value || "").trim());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function encodeInviteTokenHash(tokenHash) {
|
|
12
|
+
const normalizedTokenHash = String(tokenHash || "")
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
if (!isSha256Hex(normalizedTokenHash)) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `${OPAQUE_INVITE_TOKEN_HASH_PREFIX}${normalizedTokenHash}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function decodeInviteTokenHash(inviteToken) {
|
|
23
|
+
const normalizedToken = normalizeInviteToken(inviteToken);
|
|
24
|
+
if (!normalizedToken.startsWith(OPAQUE_INVITE_TOKEN_HASH_PREFIX)) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tokenHash = normalizedToken.slice(OPAQUE_INVITE_TOKEN_HASH_PREFIX.length).trim().toLowerCase();
|
|
29
|
+
return isSha256Hex(tokenHash) ? tokenHash : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
OPAQUE_INVITE_TOKEN_HASH_PREFIX,
|
|
34
|
+
normalizeInviteToken,
|
|
35
|
+
isSha256Hex,
|
|
36
|
+
encodeInviteTokenHash,
|
|
37
|
+
decodeInviteTokenHash
|
|
38
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const OAUTH_PROVIDER_ID_PATTERN = "^[a-z0-9][a-z0-9_-]{1,31}$";
|
|
2
|
+
const OAUTH_PROVIDER_ID_REGEX = new RegExp(OAUTH_PROVIDER_ID_PATTERN);
|
|
3
|
+
|
|
4
|
+
function normalizeOAuthProviderId(value, { fallback = null } = {}) {
|
|
5
|
+
const normalized = String(value || "")
|
|
6
|
+
.trim()
|
|
7
|
+
.toLowerCase();
|
|
8
|
+
|
|
9
|
+
if (OAUTH_PROVIDER_ID_REGEX.test(normalized)) {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const fallbackNormalized = String(fallback || "")
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase();
|
|
16
|
+
if (OAUTH_PROVIDER_ID_REGEX.test(fallbackNormalized)) {
|
|
17
|
+
return fallbackNormalized;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isValidOAuthProviderId(value) {
|
|
24
|
+
return Boolean(normalizeOAuthProviderId(value, { fallback: null }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeOAuthProviderList(value, { fallback = [] } = {}) {
|
|
28
|
+
const source = Array.isArray(value)
|
|
29
|
+
? value
|
|
30
|
+
: typeof value === "string"
|
|
31
|
+
? value.split(",")
|
|
32
|
+
: value == null
|
|
33
|
+
? []
|
|
34
|
+
: [value];
|
|
35
|
+
|
|
36
|
+
const normalized = [];
|
|
37
|
+
for (const entry of source) {
|
|
38
|
+
const providerId = normalizeOAuthProviderId(entry, { fallback: null });
|
|
39
|
+
if (!providerId || normalized.includes(providerId)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
normalized.push(providerId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (normalized.length > 0) {
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!Array.isArray(fallback)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (fallback.length < 1) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return normalizeOAuthProviderList(fallback, { fallback: [] });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
OAUTH_PROVIDER_ID_PATTERN,
|
|
62
|
+
OAUTH_PROVIDER_ID_REGEX,
|
|
63
|
+
normalizeOAuthProviderId,
|
|
64
|
+
isValidOAuthProviderId,
|
|
65
|
+
normalizeOAuthProviderList
|
|
66
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function normalizeAuthApi(authApi) {
|
|
2
|
+
if (!authApi || typeof authApi !== "object") {
|
|
3
|
+
throw new TypeError("runAuthSignOutFlow requires authApi.");
|
|
4
|
+
}
|
|
5
|
+
if (typeof authApi.logout !== "function") {
|
|
6
|
+
throw new TypeError("runAuthSignOutFlow requires authApi.logout().");
|
|
7
|
+
}
|
|
8
|
+
return authApi;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function runAuthSignOutFlow({ authApi, clearCsrfTokenCache = null, afterSignOut = null } = {}) {
|
|
12
|
+
const normalizedAuthApi = normalizeAuthApi(authApi);
|
|
13
|
+
const clearFn = typeof clearCsrfTokenCache === "function" ? clearCsrfTokenCache : null;
|
|
14
|
+
const afterFn = typeof afterSignOut === "function" ? afterSignOut : null;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await normalizedAuthApi.logout();
|
|
18
|
+
} finally {
|
|
19
|
+
if (clearFn) {
|
|
20
|
+
clearFn();
|
|
21
|
+
}
|
|
22
|
+
if (afterFn) {
|
|
23
|
+
await afterFn();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { runAuthSignOutFlow };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createAuthActionContextContributor } from "../src/server/lib/actionContextContributor.js";
|
|
4
|
+
|
|
5
|
+
test("auth action context contributor skips empty placeholder values", () => {
|
|
6
|
+
const contributor = createAuthActionContextContributor();
|
|
7
|
+
|
|
8
|
+
const contribution = contributor.contribute({
|
|
9
|
+
request: {
|
|
10
|
+
user: null,
|
|
11
|
+
workspace: null,
|
|
12
|
+
membership: null,
|
|
13
|
+
permissions: []
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
assert.deepEqual(contribution, {});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("auth action context contributor contributes real request context values", () => {
|
|
21
|
+
const contributor = createAuthActionContextContributor();
|
|
22
|
+
|
|
23
|
+
const request = {
|
|
24
|
+
user: {
|
|
25
|
+
id: 7
|
|
26
|
+
},
|
|
27
|
+
workspace: {
|
|
28
|
+
id: 11
|
|
29
|
+
},
|
|
30
|
+
membership: {
|
|
31
|
+
roleId: "owner"
|
|
32
|
+
},
|
|
33
|
+
permissions: ["workspace.settings.update", "", " "]
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const contribution = contributor.contribute({ request });
|
|
37
|
+
|
|
38
|
+
assert.deepEqual(contribution, {
|
|
39
|
+
actor: request.user,
|
|
40
|
+
workspace: request.workspace,
|
|
41
|
+
membership: request.membership,
|
|
42
|
+
permissions: ["workspace.settings.update"]
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { createApi } from "../src/client/authApi.js";
|
|
5
|
+
|
|
6
|
+
test("authApi exposes the expected methods and request routes", async () => {
|
|
7
|
+
const calls = [];
|
|
8
|
+
const api = createApi({
|
|
9
|
+
request: async (url, options = {}) => {
|
|
10
|
+
calls.push({ url, options });
|
|
11
|
+
return { ok: true };
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.deepEqual(Object.keys(api), [
|
|
16
|
+
"session",
|
|
17
|
+
"register",
|
|
18
|
+
"login",
|
|
19
|
+
"requestOtp",
|
|
20
|
+
"verifyOtp",
|
|
21
|
+
"oauthStartUrl",
|
|
22
|
+
"oauthComplete",
|
|
23
|
+
"requestPasswordReset",
|
|
24
|
+
"completePasswordRecovery",
|
|
25
|
+
"resetPassword",
|
|
26
|
+
"logout"
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
await api.session();
|
|
30
|
+
await api.login({ email: "x@example.com" });
|
|
31
|
+
await api.logout();
|
|
32
|
+
|
|
33
|
+
assert.equal(calls[0].url, "/api/session");
|
|
34
|
+
assert.equal(calls[1].url, "/api/login");
|
|
35
|
+
assert.equal(calls[1].options.method, "POST");
|
|
36
|
+
assert.equal(calls[2].url, "/api/logout");
|
|
37
|
+
assert.equal(calls[2].options.method, "POST");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("authApi oauthStartUrl builds provider path with optional returnTo", () => {
|
|
41
|
+
const api = createApi({
|
|
42
|
+
request: async () => ({})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.equal(api.oauthStartUrl("GitHub"), "/api/oauth/github/start");
|
|
46
|
+
assert.equal(api.oauthStartUrl("github", { returnTo: "/console" }), "/api/oauth/github/start?returnTo=%2Fconsole");
|
|
47
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AUTH_METHOD_IDS,
|
|
6
|
+
AUTH_METHOD_KINDS,
|
|
7
|
+
buildAuthMethodDefinitions,
|
|
8
|
+
buildAuthMethodIds,
|
|
9
|
+
buildOAuthMethodDefinitions,
|
|
10
|
+
buildOAuthMethodId,
|
|
11
|
+
findAuthMethodDefinition,
|
|
12
|
+
parseAuthMethodId
|
|
13
|
+
} from "../src/shared/authMethods.js";
|
|
14
|
+
import {
|
|
15
|
+
isValidOAuthProviderId,
|
|
16
|
+
normalizeOAuthProviderId,
|
|
17
|
+
normalizeOAuthProviderList
|
|
18
|
+
} from "../src/shared/oauthProviders.js";
|
|
19
|
+
|
|
20
|
+
test("oauth provider helpers normalize ids and lists", () => {
|
|
21
|
+
assert.equal(normalizeOAuthProviderId(" Google "), "google");
|
|
22
|
+
assert.equal(normalizeOAuthProviderId(""), null);
|
|
23
|
+
assert.equal(normalizeOAuthProviderId("x"), null);
|
|
24
|
+
assert.equal(normalizeOAuthProviderId("", { fallback: "github" }), "github");
|
|
25
|
+
|
|
26
|
+
assert.equal(isValidOAuthProviderId("github"), true);
|
|
27
|
+
assert.equal(isValidOAuthProviderId("bad provider"), false);
|
|
28
|
+
|
|
29
|
+
assert.deepEqual(normalizeOAuthProviderList([" google ", "github", "google", "bad provider"]), [
|
|
30
|
+
"google",
|
|
31
|
+
"github"
|
|
32
|
+
]);
|
|
33
|
+
assert.deepEqual(normalizeOAuthProviderList("google, github, invalid provider"), ["google", "github"]);
|
|
34
|
+
assert.deepEqual(normalizeOAuthProviderList("", { fallback: ["google"] }), ["google"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("auth method builders build provider-aware oauth method definitions", () => {
|
|
38
|
+
const oauthMethods = buildOAuthMethodDefinitions([
|
|
39
|
+
{ id: "google", label: "Google" },
|
|
40
|
+
{ id: "github", label: "GitHub" },
|
|
41
|
+
{ id: "google", label: "Duplicate" },
|
|
42
|
+
{ id: "bad provider", label: "Invalid" }
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
oauthMethods.map((method) => method.id),
|
|
47
|
+
["oauth:google", "oauth:github"]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const methods = buildAuthMethodDefinitions({
|
|
51
|
+
oauthProviders: [
|
|
52
|
+
{ id: "google", label: "Google" },
|
|
53
|
+
{ id: "github", label: "GitHub" }
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(methods.map((method) => method.id), ["password", "email_otp", "oauth:google", "oauth:github"]);
|
|
57
|
+
|
|
58
|
+
const ids = buildAuthMethodIds({
|
|
59
|
+
oauthProviders: [
|
|
60
|
+
{ id: "google", label: "Google" },
|
|
61
|
+
{ id: "github", label: "GitHub" }
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(ids, ["password", "email_otp", "oauth:google", "oauth:github"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("auth method helpers parse generic oauth ids while definition lookup remains catalog-based", () => {
|
|
68
|
+
assert.deepEqual(parseAuthMethodId("password"), {
|
|
69
|
+
id: "password",
|
|
70
|
+
kind: "password",
|
|
71
|
+
provider: "email"
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.deepEqual(parseAuthMethodId("oauth:github"), {
|
|
75
|
+
id: "oauth:github",
|
|
76
|
+
kind: "oauth",
|
|
77
|
+
provider: "github"
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(parseAuthMethodId("oauth:bad provider"), null);
|
|
81
|
+
assert.equal(parseAuthMethodId("unknown"), null);
|
|
82
|
+
|
|
83
|
+
assert.equal(findAuthMethodDefinition("oauth:github"), null);
|
|
84
|
+
assert.equal(
|
|
85
|
+
findAuthMethodDefinition("oauth:github", {
|
|
86
|
+
oauthProviders: [{ id: "github", label: "GitHub" }]
|
|
87
|
+
})?.label,
|
|
88
|
+
"GitHub"
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(AUTH_METHOD_IDS, ["password", "email_otp"]);
|
|
92
|
+
assert.deepEqual(AUTH_METHOD_KINDS, ["password", "otp", "oauth"]);
|
|
93
|
+
assert.equal(buildOAuthMethodId("google"), "oauth:google");
|
|
94
|
+
assert.equal(buildOAuthMethodId("x"), null);
|
|
95
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { AUTH_PATHS, buildAuthOauthStartPath } from "../src/shared/authPaths.js";
|
|
5
|
+
|
|
6
|
+
test("AUTH_PATHS defines canonical auth endpoint paths", () => {
|
|
7
|
+
assert.equal(Object.isFrozen(AUTH_PATHS), true);
|
|
8
|
+
assert.equal(AUTH_PATHS.LOGIN, "/api/login");
|
|
9
|
+
assert.equal(AUTH_PATHS.LOGOUT, "/api/logout");
|
|
10
|
+
assert.equal(AUTH_PATHS.SESSION, "/api/session");
|
|
11
|
+
assert.equal(AUTH_PATHS.OAUTH_START_TEMPLATE, "/api/oauth/:provider/start");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("buildAuthOauthStartPath normalizes and encodes provider id", () => {
|
|
15
|
+
assert.equal(buildAuthOauthStartPath("GitHub"), "/api/oauth/github/start");
|
|
16
|
+
assert.equal(buildAuthOauthStartPath("Git Hub"), "/api/oauth/git%20hub/start");
|
|
17
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { authRegisterCommand } from "../src/shared/commands/authRegisterCommand.js";
|
|
4
|
+
import { authLoginPasswordCommand } from "../src/shared/commands/authLoginPasswordCommand.js";
|
|
5
|
+
import { authLoginOtpRequestCommand } from "../src/shared/commands/authLoginOtpRequestCommand.js";
|
|
6
|
+
import { authLoginOtpVerifyCommand } from "../src/shared/commands/authLoginOtpVerifyCommand.js";
|
|
7
|
+
import { authLoginOAuthStartCommand } from "../src/shared/commands/authLoginOAuthStartCommand.js";
|
|
8
|
+
import { authLoginOAuthCompleteCommand } from "../src/shared/commands/authLoginOAuthCompleteCommand.js";
|
|
9
|
+
import { authPasswordResetRequestCommand } from "../src/shared/commands/authPasswordResetRequestCommand.js";
|
|
10
|
+
import { authPasswordRecoveryCompleteCommand } from "../src/shared/commands/authPasswordRecoveryCompleteCommand.js";
|
|
11
|
+
import { authPasswordResetCommand } from "../src/shared/commands/authPasswordResetCommand.js";
|
|
12
|
+
import { authLogoutCommand } from "../src/shared/commands/authLogoutCommand.js";
|
|
13
|
+
import { authSessionReadCommand } from "../src/shared/commands/authSessionReadCommand.js";
|
|
14
|
+
|
|
15
|
+
test("auth commands expose canonical operation validator messages", () => {
|
|
16
|
+
const commands = {
|
|
17
|
+
authRegisterCommand,
|
|
18
|
+
authLoginPasswordCommand,
|
|
19
|
+
authLoginOtpRequestCommand,
|
|
20
|
+
authLoginOtpVerifyCommand,
|
|
21
|
+
authLoginOAuthStartCommand,
|
|
22
|
+
authLoginOAuthCompleteCommand,
|
|
23
|
+
authPasswordResetRequestCommand,
|
|
24
|
+
authPasswordRecoveryCompleteCommand,
|
|
25
|
+
authPasswordResetCommand,
|
|
26
|
+
authLogoutCommand,
|
|
27
|
+
authSessionReadCommand
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (const [label, command] of Object.entries(commands)) {
|
|
31
|
+
assert.equal(typeof command.operation?.messages, "object", `${label}.operation.messages must be an object.`);
|
|
32
|
+
}
|
|
33
|
+
});
|