@jskit-ai/auth-core 0.1.54 → 0.1.55
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 +2 -2
- package/package.json +3 -3
- package/src/server/authPolicyContextResolverRegistry.js +5 -0
- package/src/server/providers/FastifyAuthPolicyServiceProvider.js +11 -27
- package/src/shared/commands/authCommandValidators.js +231 -191
- package/src/shared/commands/authDevLoginAsCommand.js +19 -19
- package/src/shared/commands/authLoginOAuthCompleteCommand.js +26 -31
- package/src/shared/commands/authLoginOAuthStartCommand.js +29 -34
- package/src/shared/commands/authLoginOtpRequestCommand.js +18 -23
- package/src/shared/commands/authLoginOtpVerifyCommand.js +29 -22
- package/src/shared/commands/authLoginPasswordCommand.js +18 -23
- package/src/shared/commands/authLogoutCommand.js +8 -7
- package/src/shared/commands/authPasswordRecoveryCompleteCommand.js +33 -23
- package/src/shared/commands/authPasswordResetCommand.js +16 -21
- package/src/shared/commands/authPasswordResetRequestCommand.js +16 -21
- package/src/shared/commands/authRegisterCommand.js +18 -23
- package/src/shared/commands/authRegisterConfirmationResendCommand.js +15 -20
- package/src/shared/commands/authSessionReadCommand.js +11 -10
- package/test/authPolicyContextResolverRegistry.test.js +39 -1
- package/test/commandValidators.test.js +68 -2
- package/test/providerRuntime.test.js +20 -8
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
authEmailFieldDefinition,
|
|
5
|
+
authPasswordFieldDefinition,
|
|
6
6
|
createCommandMessages,
|
|
7
|
-
|
|
7
|
+
registerOutputValidator
|
|
8
8
|
} from "./authCommandValidators.js";
|
|
9
9
|
|
|
10
10
|
const AUTH_REGISTER_MESSAGES = createCommandMessages({
|
|
@@ -23,35 +23,30 @@ const AUTH_REGISTER_MESSAGES = createCommandMessages({
|
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
const authRegisterBodyValidator =
|
|
27
|
-
schema:
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
additionalProperties: false
|
|
34
|
-
}
|
|
35
|
-
),
|
|
36
|
-
normalize: normalizeObjectInput,
|
|
26
|
+
const authRegisterBodyValidator = deepFreeze({
|
|
27
|
+
schema: createSchema({
|
|
28
|
+
email: { ...authEmailFieldDefinition, required: true },
|
|
29
|
+
password: { ...authPasswordFieldDefinition, required: true }
|
|
30
|
+
}),
|
|
31
|
+
mode: "create",
|
|
37
32
|
messages: AUTH_REGISTER_MESSAGES
|
|
38
33
|
});
|
|
39
34
|
|
|
40
|
-
const authRegisterCommand =
|
|
35
|
+
const authRegisterCommand = deepFreeze({
|
|
41
36
|
command: "auth.register",
|
|
42
|
-
operation:
|
|
37
|
+
operation: {
|
|
43
38
|
method: "POST",
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
body: authRegisterBodyValidator,
|
|
40
|
+
response: registerOutputValidator,
|
|
46
41
|
messages: AUTH_REGISTER_MESSAGES,
|
|
47
42
|
idempotent: false,
|
|
48
|
-
invalidates:
|
|
49
|
-
}
|
|
43
|
+
invalidates: ["auth.session.read"]
|
|
44
|
+
}
|
|
50
45
|
});
|
|
51
46
|
|
|
52
47
|
export {
|
|
53
48
|
authRegisterBodyValidator,
|
|
54
|
-
|
|
49
|
+
registerOutputValidator,
|
|
55
50
|
AUTH_REGISTER_MESSAGES,
|
|
56
51
|
authRegisterCommand
|
|
57
52
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
authEmailFieldDefinition,
|
|
5
5
|
createCommandMessages,
|
|
6
|
-
|
|
6
|
+
okMessageOutputValidator
|
|
7
7
|
} from "./authCommandValidators.js";
|
|
8
8
|
|
|
9
9
|
const AUTH_REGISTER_CONFIRMATION_RESEND_MESSAGES = createCommandMessages({
|
|
@@ -17,29 +17,24 @@ const AUTH_REGISTER_CONFIRMATION_RESEND_MESSAGES = createCommandMessages({
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
const authRegisterConfirmationResendBodyValidator =
|
|
21
|
-
schema:
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
additionalProperties: false
|
|
27
|
-
}
|
|
28
|
-
),
|
|
29
|
-
normalize: normalizeObjectInput,
|
|
20
|
+
const authRegisterConfirmationResendBodyValidator = deepFreeze({
|
|
21
|
+
schema: createSchema({
|
|
22
|
+
email: { ...authEmailFieldDefinition, required: true }
|
|
23
|
+
}),
|
|
24
|
+
mode: "create",
|
|
30
25
|
messages: AUTH_REGISTER_CONFIRMATION_RESEND_MESSAGES
|
|
31
26
|
});
|
|
32
27
|
|
|
33
|
-
const authRegisterConfirmationResendCommand =
|
|
28
|
+
const authRegisterConfirmationResendCommand = deepFreeze({
|
|
34
29
|
command: "auth.register.confirmation.resend",
|
|
35
|
-
operation:
|
|
30
|
+
operation: {
|
|
36
31
|
method: "POST",
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
body: authRegisterConfirmationResendBodyValidator,
|
|
33
|
+
response: okMessageOutputValidator,
|
|
39
34
|
messages: AUTH_REGISTER_CONFIRMATION_RESEND_MESSAGES,
|
|
40
35
|
idempotent: false,
|
|
41
|
-
invalidates:
|
|
42
|
-
}
|
|
36
|
+
invalidates: []
|
|
37
|
+
}
|
|
43
38
|
});
|
|
44
39
|
|
|
45
40
|
export {
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createCommandMessages,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
sessionOutputValidator,
|
|
4
|
+
sessionUnavailableOutputValidator
|
|
5
5
|
} from "./authCommandValidators.js";
|
|
6
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
6
7
|
|
|
7
8
|
const AUTH_SESSION_READ_MESSAGES = createCommandMessages();
|
|
8
9
|
|
|
9
|
-
const authSessionReadCommand =
|
|
10
|
+
const authSessionReadCommand = deepFreeze({
|
|
10
11
|
command: "auth.session.read",
|
|
11
|
-
operation:
|
|
12
|
+
operation: {
|
|
12
13
|
method: "GET",
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
response: sessionOutputValidator,
|
|
15
|
+
unavailableResponse: sessionUnavailableOutputValidator,
|
|
15
16
|
messages: AUTH_SESSION_READ_MESSAGES,
|
|
16
17
|
idempotent: true,
|
|
17
|
-
invalidates:
|
|
18
|
-
}
|
|
18
|
+
invalidates: []
|
|
19
|
+
}
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
export {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
sessionOutputValidator,
|
|
24
|
+
sessionUnavailableOutputValidator,
|
|
24
25
|
AUTH_SESSION_READ_MESSAGES,
|
|
25
26
|
authSessionReadCommand
|
|
26
27
|
};
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
AUTH_POLICY_CONTEXT_RESOLVER_TAG,
|
|
6
6
|
composeAuthPolicyContextResolvers,
|
|
7
7
|
registerAuthPolicyContextResolver,
|
|
8
|
-
resolveAuthPolicyContextResolvers
|
|
8
|
+
resolveAuthPolicyContextResolvers,
|
|
9
|
+
resolveComposedAuthPolicyContextResolver
|
|
9
10
|
} from "../src/server/authPolicyContextResolverRegistry.js";
|
|
10
11
|
|
|
11
12
|
test("auth policy context resolver registry resolves resolvers in order", async () => {
|
|
@@ -54,3 +55,40 @@ test("auth policy context resolver registry resolves resolvers in order", async
|
|
|
54
55
|
test("auth policy context resolver registry exports canonical tag", () => {
|
|
55
56
|
assert.equal(AUTH_POLICY_CONTEXT_RESOLVER_TAG, "jskit.auth.policy.context.resolvers");
|
|
56
57
|
});
|
|
58
|
+
|
|
59
|
+
test("auth policy context resolver registry resolves composed resolver directly from scope", async () => {
|
|
60
|
+
const app = createApplication();
|
|
61
|
+
|
|
62
|
+
registerAuthPolicyContextResolver(app, "test.auth.policy.context.permissions", () => ({
|
|
63
|
+
resolverId: "permissions",
|
|
64
|
+
order: 20,
|
|
65
|
+
async resolveAuthPolicyContext() {
|
|
66
|
+
return {
|
|
67
|
+
permissions: ["alpha.read"]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
registerAuthPolicyContextResolver(app, "test.auth.policy.context.workspace", () => ({
|
|
73
|
+
resolverId: "workspace",
|
|
74
|
+
order: 10,
|
|
75
|
+
async resolveAuthPolicyContext() {
|
|
76
|
+
return {
|
|
77
|
+
workspace: { id: "11" },
|
|
78
|
+
membership: { roleSid: "member" },
|
|
79
|
+
permissions: ["workspace.read"]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const resolveContext = resolveComposedAuthPolicyContextResolver(app);
|
|
85
|
+
const context = await resolveContext({
|
|
86
|
+
actor: { id: "7" }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.deepEqual(context, {
|
|
90
|
+
workspace: { id: "11" },
|
|
91
|
+
membership: { roleSid: "member" },
|
|
92
|
+
permissions: ["workspace.read", "alpha.read"]
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
|
|
3
4
|
import { authRegisterCommand } from "../src/shared/commands/authRegisterCommand.js";
|
|
4
5
|
import { authRegisterConfirmationResendCommand } from "../src/shared/commands/authRegisterConfirmationResendCommand.js";
|
|
5
6
|
import { authLoginPasswordCommand } from "../src/shared/commands/authLoginPasswordCommand.js";
|
|
@@ -35,11 +36,76 @@ test("auth commands expose canonical operation validator messages", () => {
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
test("oauth complete command allows provider-less session-pair callbacks", () => {
|
|
38
|
-
const bodySchema =
|
|
39
|
+
const bodySchema = resolveStructuredSchemaTransportSchema(
|
|
40
|
+
authLoginOAuthCompleteCommand.operation.body,
|
|
41
|
+
{
|
|
42
|
+
context: "auth login oauth complete body",
|
|
43
|
+
defaultMode: "patch"
|
|
44
|
+
}
|
|
45
|
+
);
|
|
39
46
|
const bodyRequired = Array.isArray(bodySchema?.required) ? bodySchema.required : [];
|
|
40
47
|
assert.equal(bodyRequired.includes("provider"), false);
|
|
41
48
|
|
|
42
|
-
const responseSchema =
|
|
49
|
+
const responseSchema = resolveStructuredSchemaTransportSchema(
|
|
50
|
+
authLoginOAuthCompleteCommand.operation.response,
|
|
51
|
+
{
|
|
52
|
+
context: "auth login oauth complete response",
|
|
53
|
+
defaultMode: "replace"
|
|
54
|
+
}
|
|
55
|
+
);
|
|
43
56
|
const responseRequired = Array.isArray(responseSchema?.required) ? responseSchema.required : [];
|
|
44
57
|
assert.equal(responseRequired.includes("provider"), false);
|
|
45
58
|
});
|
|
59
|
+
|
|
60
|
+
test("auth command body validators lowercase email inputs", () => {
|
|
61
|
+
const commands = [
|
|
62
|
+
authRegisterCommand,
|
|
63
|
+
authRegisterConfirmationResendCommand,
|
|
64
|
+
authLoginPasswordCommand,
|
|
65
|
+
authLoginOtpRequestCommand,
|
|
66
|
+
authLoginOtpVerifyCommand,
|
|
67
|
+
authPasswordResetRequestCommand
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const command of commands) {
|
|
71
|
+
const result = command.operation.body.schema.patch({
|
|
72
|
+
email: " ADA@EXAMPLE.COM "
|
|
73
|
+
});
|
|
74
|
+
if (Object.hasOwn(result.validatedObject, "email")) {
|
|
75
|
+
assert.equal(result.validatedObject.email, "ada@example.com");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("auth session and oauth start commands expose explicit nested response schemas", () => {
|
|
81
|
+
const sessionResponseSchema = resolveStructuredSchemaTransportSchema(
|
|
82
|
+
authSessionReadCommand.operation.response,
|
|
83
|
+
{
|
|
84
|
+
context: "auth session read response",
|
|
85
|
+
defaultMode: "replace"
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
const sessionUnavailableSchema = resolveStructuredSchemaTransportSchema(
|
|
89
|
+
authSessionReadCommand.operation.unavailableResponse,
|
|
90
|
+
{
|
|
91
|
+
context: "auth session read unavailable response",
|
|
92
|
+
defaultMode: "replace"
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
const oauthStartResponseSchema = resolveStructuredSchemaTransportSchema(
|
|
96
|
+
authLoginOAuthStartCommand.operation.response,
|
|
97
|
+
{
|
|
98
|
+
context: "auth login oauth start response",
|
|
99
|
+
defaultMode: "replace"
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
assert.equal(sessionResponseSchema.properties.oauthProviders.type, "array");
|
|
104
|
+
assert.equal(sessionResponseSchema.properties.oauthProviders.items["x-json-rest-schema"]?.castType, "object");
|
|
105
|
+
assert.equal(Array.isArray(sessionResponseSchema.properties.oauthProviders.items.allOf), true);
|
|
106
|
+
assert.match(sessionResponseSchema.properties.oauthProviders.items.allOf[0]?.$ref || "", /^#\/definitions\//);
|
|
107
|
+
assert.equal(Array.isArray(sessionUnavailableSchema.properties.oauthDefaultProvider.anyOf), true);
|
|
108
|
+
assert.equal(oauthStartResponseSchema.properties.provider.type, "string");
|
|
109
|
+
assert.equal(oauthStartResponseSchema.properties.returnTo.type, "string");
|
|
110
|
+
assert.equal(oauthStartResponseSchema.properties.url.type, "string");
|
|
111
|
+
});
|
|
@@ -50,6 +50,8 @@ test("FastifyAuthPolicyServiceProvider registers auth policy plugin through prov
|
|
|
50
50
|
|
|
51
51
|
test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolver", async () => {
|
|
52
52
|
const { fastify, state } = createFakeFastifyPolicyRuntime();
|
|
53
|
+
const makeCalls = [];
|
|
54
|
+
const resolveTagCalls = [];
|
|
53
55
|
const bag = new Map([
|
|
54
56
|
["jskit.fastify", fastify],
|
|
55
57
|
["jskit.env", { NODE_ENV: "test" }],
|
|
@@ -65,14 +67,6 @@ test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolv
|
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
|
-
],
|
|
69
|
-
[
|
|
70
|
-
"auth.policy.contextResolver",
|
|
71
|
-
async ({ actor, request }) => ({
|
|
72
|
-
workspace: { id: 11, slug: String(request?.params?.workspaceSlug || "").toLowerCase() },
|
|
73
|
-
membership: { roleSid: "member" },
|
|
74
|
-
permissions: actor?.id === 7 ? ["projects.read"] : []
|
|
75
|
-
})
|
|
76
70
|
]
|
|
77
71
|
]);
|
|
78
72
|
|
|
@@ -81,17 +75,30 @@ test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolv
|
|
|
81
75
|
return bag.has(token);
|
|
82
76
|
},
|
|
83
77
|
make(token) {
|
|
78
|
+
makeCalls.push(String(token));
|
|
84
79
|
if (!bag.has(token)) {
|
|
85
80
|
throw new Error(`Missing token ${String(token)}`);
|
|
86
81
|
}
|
|
87
82
|
return bag.get(token);
|
|
88
83
|
},
|
|
89
84
|
resolveTag(tag) {
|
|
85
|
+
resolveTagCalls.push(String(tag));
|
|
90
86
|
if (tag !== AUTH_POLICY_CONTEXT_RESOLVER_TAG) {
|
|
91
87
|
return [];
|
|
92
88
|
}
|
|
93
89
|
|
|
94
90
|
return [
|
|
91
|
+
{
|
|
92
|
+
resolverId: "workspace",
|
|
93
|
+
order: 10,
|
|
94
|
+
async resolveAuthPolicyContext({ actor, request }) {
|
|
95
|
+
return {
|
|
96
|
+
workspace: { id: 11, slug: String(request?.params?.workspaceSlug || "").toLowerCase() },
|
|
97
|
+
membership: { roleSid: "member" },
|
|
98
|
+
permissions: actor?.id === 7 ? ["projects.read"] : []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
},
|
|
95
102
|
async () => ({
|
|
96
103
|
permissions: ["settings.manage"]
|
|
97
104
|
})
|
|
@@ -103,6 +110,9 @@ test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolv
|
|
|
103
110
|
provider.register(app);
|
|
104
111
|
await provider.boot(app);
|
|
105
112
|
|
|
113
|
+
assert.deepEqual(makeCalls, ["jskit.env", "jskit.fastify"]);
|
|
114
|
+
assert.deepEqual(resolveTagCalls, []);
|
|
115
|
+
|
|
106
116
|
const request = {
|
|
107
117
|
method: "GET",
|
|
108
118
|
raw: { url: "/api/w/acme/projects" },
|
|
@@ -117,6 +127,8 @@ test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolv
|
|
|
117
127
|
};
|
|
118
128
|
|
|
119
129
|
await state.preHandler(request, {});
|
|
130
|
+
assert.ok(makeCalls.includes("authService"));
|
|
131
|
+
assert.deepEqual(resolveTagCalls, [AUTH_POLICY_CONTEXT_RESOLVER_TAG]);
|
|
120
132
|
assert.equal(request.workspace?.id, 11);
|
|
121
133
|
assert.equal(request.workspace?.slug, "acme");
|
|
122
134
|
assert.equal(request.membership?.roleSid, "member");
|