@jskit-ai/auth-core 0.1.97 → 0.1.100
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 +9 -4
- package/package.json +9 -2
- package/src/server/actions/auth.contributor.js +263 -0
- package/src/server/authActor.js +96 -0
- package/src/server/lib/index.js +12 -0
- package/src/server/providers/AuthActionsServiceProvider.js +67 -0
- package/src/server/providers/FastifyAuthPolicyServiceProvider.js +7 -6
- package/src/server/services/authSessionEventsService.js +30 -0
- package/src/server/unsupportedOperation.js +28 -0
- package/src/shared/authCapabilities.js +184 -0
- package/src/shared/authSecurityStatus.js +111 -0
- package/src/shared/commands/authCommandValidators.js +115 -0
- package/src/shared/index.js +12 -0
- package/test/authActions.test.js +174 -0
- package/test/authContract.test.js +171 -0
- package/test/commandValidators.test.js +5 -0
- package/test/providerRuntime.test.js +43 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
normalizeAuthCapabilities,
|
|
5
|
+
getCapabilityFeature,
|
|
6
|
+
isAuthOperationSupported
|
|
7
|
+
} from "../src/shared/authCapabilities.js";
|
|
8
|
+
import {
|
|
9
|
+
buildSecurityStatusFromAuthMethodsStatus,
|
|
10
|
+
normalizeAuthSecurityStatus
|
|
11
|
+
} from "../src/shared/authSecurityStatus.js";
|
|
12
|
+
import {
|
|
13
|
+
buildLegacyProfileFromActor,
|
|
14
|
+
normalizeAuthActor,
|
|
15
|
+
normalizeAuthResult
|
|
16
|
+
} from "../src/server/authActor.js";
|
|
17
|
+
import {
|
|
18
|
+
AUTH_OPERATION_UNSUPPORTED_CODE,
|
|
19
|
+
createUnsupportedAuthOperationError,
|
|
20
|
+
isUnsupportedAuthOperationError
|
|
21
|
+
} from "../src/server/unsupportedOperation.js";
|
|
22
|
+
|
|
23
|
+
test("normalizeAuthCapabilities returns one provider-neutral feature shape", () => {
|
|
24
|
+
const capabilities = normalizeAuthCapabilities({
|
|
25
|
+
provider: {
|
|
26
|
+
id: "LOCAL",
|
|
27
|
+
label: ""
|
|
28
|
+
},
|
|
29
|
+
features: {
|
|
30
|
+
password: {
|
|
31
|
+
login: true,
|
|
32
|
+
register: true,
|
|
33
|
+
change: true
|
|
34
|
+
},
|
|
35
|
+
passwordRecovery: {
|
|
36
|
+
request: true,
|
|
37
|
+
complete: true,
|
|
38
|
+
delivery: "smtp"
|
|
39
|
+
},
|
|
40
|
+
oauthLogin: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
providers: [
|
|
43
|
+
{ id: "Google", label: "Google" },
|
|
44
|
+
"github",
|
|
45
|
+
"bad provider"
|
|
46
|
+
],
|
|
47
|
+
defaultProvider: "github"
|
|
48
|
+
},
|
|
49
|
+
providerLinking: {
|
|
50
|
+
start: true
|
|
51
|
+
},
|
|
52
|
+
securityStatus: true
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.equal(capabilities.provider.id, "local");
|
|
57
|
+
assert.equal(capabilities.provider.label, "Local");
|
|
58
|
+
assert.equal(capabilities.features.password.login, true);
|
|
59
|
+
assert.equal(capabilities.features.password.methodToggle, false);
|
|
60
|
+
assert.equal(capabilities.features.passwordRecovery.delivery, "smtp");
|
|
61
|
+
assert.deepEqual(capabilities.features.oauthLogin.providers, [
|
|
62
|
+
{ id: "google", label: "Google" },
|
|
63
|
+
{ id: "github", label: "github" }
|
|
64
|
+
]);
|
|
65
|
+
assert.equal(capabilities.features.oauthLogin.defaultProvider, "github");
|
|
66
|
+
assert.equal(getCapabilityFeature(capabilities, "providerLinking.start"), true);
|
|
67
|
+
assert.equal(isAuthOperationSupported(capabilities, "startProviderLink"), true);
|
|
68
|
+
assert.equal(isAuthOperationSupported(capabilities, "unlinkProvider"), false);
|
|
69
|
+
assert.equal(isAuthOperationSupported(capabilities, "resetPassword"), true);
|
|
70
|
+
assert.equal(
|
|
71
|
+
isAuthOperationSupported(
|
|
72
|
+
{
|
|
73
|
+
features: {
|
|
74
|
+
password: {
|
|
75
|
+
change: true
|
|
76
|
+
},
|
|
77
|
+
passwordRecovery: {
|
|
78
|
+
complete: false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"resetPassword"
|
|
83
|
+
),
|
|
84
|
+
false
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("normalizeAuthActor builds the stable actor and legacy profile bridge", () => {
|
|
89
|
+
const actor = normalizeAuthActor({
|
|
90
|
+
authProvider: "supabase",
|
|
91
|
+
authProviderUserSid: "abc-123",
|
|
92
|
+
email: " ADA@example.COM ",
|
|
93
|
+
displayName: "Ada",
|
|
94
|
+
appUserId: 42,
|
|
95
|
+
profileSource: "users"
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert.deepEqual(actor, {
|
|
99
|
+
authIdentityId: "supabase:abc-123",
|
|
100
|
+
provider: "supabase",
|
|
101
|
+
providerUserId: "abc-123",
|
|
102
|
+
email: "ada@example.com",
|
|
103
|
+
displayName: "Ada",
|
|
104
|
+
appUserId: "42",
|
|
105
|
+
profileSource: "users"
|
|
106
|
+
});
|
|
107
|
+
assert.deepEqual(buildLegacyProfileFromActor(actor), {
|
|
108
|
+
id: "42",
|
|
109
|
+
email: "ada@example.com",
|
|
110
|
+
displayName: "Ada",
|
|
111
|
+
authProvider: "supabase",
|
|
112
|
+
authProviderUserSid: "abc-123"
|
|
113
|
+
});
|
|
114
|
+
assert.deepEqual(normalizeAuthResult({ authenticated: true, actor }).profile, buildLegacyProfileFromActor(actor));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("normalizeAuthActor preserves opaque app user ids", () => {
|
|
118
|
+
const actor = normalizeAuthActor({
|
|
119
|
+
provider: "local",
|
|
120
|
+
providerUserId: "provider-user-1",
|
|
121
|
+
email: "local@example.com",
|
|
122
|
+
appUserId: "app-user-1",
|
|
123
|
+
profileSource: "users"
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
assert.equal(actor.appUserId, "app-user-1");
|
|
127
|
+
assert.equal(buildLegacyProfileFromActor(actor).id, "app-user-1");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("normalizeAuthSecurityStatus emits policy and legacy authPolicy aliases", () => {
|
|
131
|
+
const status = buildSecurityStatusFromAuthMethodsStatus(
|
|
132
|
+
{
|
|
133
|
+
methods: [
|
|
134
|
+
{
|
|
135
|
+
id: "password",
|
|
136
|
+
kind: "password",
|
|
137
|
+
configured: true,
|
|
138
|
+
enabled: true,
|
|
139
|
+
canDisable: false
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
minimumEnabledMethods: 1,
|
|
143
|
+
enabledMethodsCount: 1
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
actions: {
|
|
147
|
+
changePassword: true,
|
|
148
|
+
signOutOtherSessions: true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
assert.deepEqual(status.policy, {
|
|
154
|
+
minimumEnabledMethods: 1,
|
|
155
|
+
enabledMethodsCount: 1
|
|
156
|
+
});
|
|
157
|
+
assert.equal(status.authPolicy, status.policy);
|
|
158
|
+
assert.equal(status.actions.changePassword, true);
|
|
159
|
+
assert.equal(status.actions.linkProvider, false);
|
|
160
|
+
assert.deepEqual(normalizeAuthSecurityStatus(status), status);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("unsupported auth operation errors are stable AppErrors", () => {
|
|
164
|
+
const error = createUnsupportedAuthOperationError("oauthStart");
|
|
165
|
+
assert.equal(error.status, 501);
|
|
166
|
+
assert.equal(error.code, AUTH_OPERATION_UNSUPPORTED_CODE);
|
|
167
|
+
assert.deepEqual(error.details, {
|
|
168
|
+
operation: "oauthStart"
|
|
169
|
+
});
|
|
170
|
+
assert.equal(isUnsupportedAuthOperationError(error), true);
|
|
171
|
+
});
|
|
@@ -104,9 +104,14 @@ test("auth session and oauth start commands expose explicit nested response sche
|
|
|
104
104
|
assert.equal(sessionResponseSchema.properties.oauthProviders.items["x-json-rest-schema"]?.castType, "object");
|
|
105
105
|
assert.equal(Array.isArray(sessionResponseSchema.properties.oauthProviders.items.allOf), true);
|
|
106
106
|
assert.match(sessionResponseSchema.properties.oauthProviders.items.allOf[0]?.$ref || "", /^#\/definitions\//);
|
|
107
|
+
assert.equal(sessionResponseSchema.properties.authCapabilities["x-json-rest-schema"]?.castType, "object");
|
|
108
|
+
assert.equal(Array.isArray(sessionResponseSchema.properties.authCapabilities.allOf), true);
|
|
109
|
+
assert.match(sessionResponseSchema.properties.authCapabilities.allOf[0]?.$ref || "", /^#\/definitions\//);
|
|
107
110
|
assert.equal(sessionResponseSchema.properties.authDenied["x-json-rest-schema"]?.castType, "object");
|
|
108
111
|
assert.equal(Array.isArray(sessionResponseSchema.properties.authDenied.allOf), true);
|
|
109
112
|
assert.match(sessionResponseSchema.properties.authDenied.allOf[0]?.$ref || "", /^#\/definitions\//);
|
|
113
|
+
assert.equal(sessionUnavailableSchema.properties.authCapabilities["x-json-rest-schema"]?.castType, "object");
|
|
114
|
+
assert.equal(Array.isArray(sessionUnavailableSchema.properties.authCapabilities.allOf), true);
|
|
110
115
|
assert.equal(Array.isArray(sessionUnavailableSchema.properties.oauthDefaultProvider.anyOf), true);
|
|
111
116
|
assert.equal(oauthStartResponseSchema.properties.provider.type, "string");
|
|
112
117
|
assert.equal(oauthStartResponseSchema.properties.returnTo.type, "string");
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { createApplication } from "@jskit-ai/kernel/_testable";
|
|
4
|
+
import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
|
|
5
|
+
import { AccessCoreServiceProvider } from "../src/server/providers/AccessCoreServiceProvider.js";
|
|
6
|
+
import { AuthActionsServiceProvider } from "../src/server/providers/AuthActionsServiceProvider.js";
|
|
3
7
|
import { FastifyAuthPolicyServiceProvider } from "../src/server/providers/FastifyAuthPolicyServiceProvider.js";
|
|
4
8
|
import { AUTH_POLICY_CONTEXT_RESOLVER_TAG } from "../src/server/authPolicyContextResolverRegistry.js";
|
|
5
9
|
import { createFakeFastifyPolicyRuntime } from "../../../tooling/testUtils/fakeFastify.mjs";
|
|
@@ -134,3 +138,42 @@ test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolv
|
|
|
134
138
|
assert.equal(request.membership?.roleSid, "member");
|
|
135
139
|
assert.deepEqual(request.permissions, ["settings.manage", "projects.read"]);
|
|
136
140
|
});
|
|
141
|
+
|
|
142
|
+
test("auth-core providers boot without a selected provider and deny protected API requests", async () => {
|
|
143
|
+
const { fastify, state } = createFakeFastifyPolicyRuntime();
|
|
144
|
+
const app = createApplication();
|
|
145
|
+
|
|
146
|
+
app.instance("appConfig", {
|
|
147
|
+
surfaceModeAll: "all",
|
|
148
|
+
surfaceDefaultId: "home",
|
|
149
|
+
surfaceDefinitions: {
|
|
150
|
+
home: { id: "home", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false }
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
app.instance("jskit.fastify", fastify);
|
|
154
|
+
app.instance("jskit.env", { NODE_ENV: "test" });
|
|
155
|
+
|
|
156
|
+
await app.start({
|
|
157
|
+
providers: [
|
|
158
|
+
ActionRuntimeServiceProvider,
|
|
159
|
+
AccessCoreServiceProvider,
|
|
160
|
+
AuthActionsServiceProvider,
|
|
161
|
+
FastifyAuthPolicyServiceProvider
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const request = {
|
|
166
|
+
method: "GET",
|
|
167
|
+
raw: { url: "/api/protected" },
|
|
168
|
+
routeOptions: {
|
|
169
|
+
config: {
|
|
170
|
+
authPolicy: "required"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await assert.rejects(
|
|
176
|
+
() => state.preHandler(request, {}),
|
|
177
|
+
/Authentication required/
|
|
178
|
+
);
|
|
179
|
+
});
|