@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,250 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createFakeFastifyPolicyRuntime } from "../../../tooling/testUtils/fakeFastify.mjs";
|
|
4
|
+
|
|
5
|
+
import { authPolicyPlugin } from "../src/server/lib/index.js";
|
|
6
|
+
|
|
7
|
+
test("requires resolveActor and hasPermission dependencies", () => {
|
|
8
|
+
assert.throws(() => authPolicyPlugin(), /resolveActor is required/);
|
|
9
|
+
assert.throws(() => authPolicyPlugin({ resolveActor() {} }), /hasPermission is required/);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("resolves csrf token headers and skips auth for non-api/public routes", async () => {
|
|
13
|
+
let actorCalls = 0;
|
|
14
|
+
const { fastify, state } = createFakeFastifyPolicyRuntime();
|
|
15
|
+
const registerPlugin = authPolicyPlugin(
|
|
16
|
+
{
|
|
17
|
+
async resolveActor() {
|
|
18
|
+
actorCalls += 1;
|
|
19
|
+
return {
|
|
20
|
+
authenticated: false,
|
|
21
|
+
actor: null,
|
|
22
|
+
transientFailure: false
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
hasPermission() {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
nodeEnv: "test"
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await registerPlugin(fastify);
|
|
35
|
+
assert.ok(state.preHandler);
|
|
36
|
+
assert.ok(state.csrfOptions);
|
|
37
|
+
const getToken = state.csrfOptions.getToken;
|
|
38
|
+
assert.equal(getToken({ headers: { "csrf-token": "a" } }), "a");
|
|
39
|
+
assert.equal(getToken({ headers: { "x-csrf-token": "b" } }), "b");
|
|
40
|
+
assert.equal(getToken({ headers: { "x-xsrf-token": "c" } }), "c");
|
|
41
|
+
assert.equal(getToken({ headers: {} }), null);
|
|
42
|
+
|
|
43
|
+
await state.preHandler({ method: "GET", raw: { url: "/health" }, routeOptions: {} }, {});
|
|
44
|
+
await state.preHandler(
|
|
45
|
+
{
|
|
46
|
+
method: "GET",
|
|
47
|
+
raw: { url: "/api/public" },
|
|
48
|
+
routeOptions: {
|
|
49
|
+
config: {
|
|
50
|
+
authPolicy: "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{}
|
|
55
|
+
);
|
|
56
|
+
assert.equal(actorCalls, 0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("propagates csrf callback error", async () => {
|
|
60
|
+
const { fastify, state } = createFakeFastifyPolicyRuntime({
|
|
61
|
+
csrfHandler(_request, _reply, done) {
|
|
62
|
+
done(new Error("csrf callback failed"));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const registerPlugin = authPolicyPlugin({
|
|
67
|
+
async resolveActor() {
|
|
68
|
+
return {
|
|
69
|
+
authenticated: false,
|
|
70
|
+
actor: null,
|
|
71
|
+
transientFailure: false
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
hasPermission() {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await registerPlugin(fastify);
|
|
80
|
+
await assert.rejects(
|
|
81
|
+
() =>
|
|
82
|
+
state.preHandler(
|
|
83
|
+
{
|
|
84
|
+
method: "POST",
|
|
85
|
+
raw: { url: "/api/public-action" },
|
|
86
|
+
routeOptions: {
|
|
87
|
+
config: {
|
|
88
|
+
authPolicy: "public"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
headers: {}
|
|
92
|
+
},
|
|
93
|
+
{}
|
|
94
|
+
),
|
|
95
|
+
/csrf callback failed/
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("enforces own policy owner checks and invalid policy guard", async () => {
|
|
100
|
+
const denyEvents = [];
|
|
101
|
+
const { fastify, state } = createFakeFastifyPolicyRuntime();
|
|
102
|
+
const registerPlugin = authPolicyPlugin({
|
|
103
|
+
async resolveActor(request) {
|
|
104
|
+
if (request.headers?.["x-profile"] === "null") {
|
|
105
|
+
return {
|
|
106
|
+
authenticated: true,
|
|
107
|
+
actor: null,
|
|
108
|
+
transientFailure: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
authenticated: true,
|
|
114
|
+
actor: {
|
|
115
|
+
id: 7
|
|
116
|
+
},
|
|
117
|
+
transientFailure: false
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
hasPermission() {
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
onPolicyDenied(event) {
|
|
124
|
+
denyEvents.push(event);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await registerPlugin(fastify);
|
|
129
|
+
|
|
130
|
+
await assert.rejects(
|
|
131
|
+
() =>
|
|
132
|
+
state.preHandler(
|
|
133
|
+
{
|
|
134
|
+
method: "GET",
|
|
135
|
+
raw: { url: "/api/own-a" },
|
|
136
|
+
headers: { "x-profile": "null" },
|
|
137
|
+
routeOptions: {
|
|
138
|
+
config: {
|
|
139
|
+
authPolicy: "own",
|
|
140
|
+
ownerResolver({ user }) {
|
|
141
|
+
assert.equal(user, null);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{}
|
|
148
|
+
),
|
|
149
|
+
/Route owner could not be resolved/
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await assert.rejects(
|
|
153
|
+
() =>
|
|
154
|
+
state.preHandler(
|
|
155
|
+
{
|
|
156
|
+
method: "GET",
|
|
157
|
+
raw: { url: "/api/own-b" },
|
|
158
|
+
headers: {},
|
|
159
|
+
params: { id: 99 },
|
|
160
|
+
routeOptions: {
|
|
161
|
+
config: {
|
|
162
|
+
authPolicy: "own",
|
|
163
|
+
ownerParam: "id",
|
|
164
|
+
userField: "id"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{}
|
|
169
|
+
),
|
|
170
|
+
/Forbidden/
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await assert.rejects(
|
|
174
|
+
() =>
|
|
175
|
+
state.preHandler(
|
|
176
|
+
{
|
|
177
|
+
method: "GET",
|
|
178
|
+
raw: { url: "/api/bad-policy" },
|
|
179
|
+
headers: {},
|
|
180
|
+
routeOptions: {
|
|
181
|
+
config: {
|
|
182
|
+
authPolicy: "unknown-policy"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{}
|
|
187
|
+
),
|
|
188
|
+
/Invalid route auth policy configuration/
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
assert.deepEqual(
|
|
192
|
+
denyEvents.map((event) => event.reason),
|
|
193
|
+
["owner_unresolved", "forbidden_owner_mismatch", "invalid_auth_policy"]
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("enforces permission checks and resolves workspace context when requested", async () => {
|
|
198
|
+
const denyEvents = [];
|
|
199
|
+
const { fastify, state } = createFakeFastifyPolicyRuntime();
|
|
200
|
+
const registerPlugin = authPolicyPlugin({
|
|
201
|
+
async resolveActor() {
|
|
202
|
+
return {
|
|
203
|
+
authenticated: true,
|
|
204
|
+
actor: {
|
|
205
|
+
id: 7
|
|
206
|
+
},
|
|
207
|
+
transientFailure: false
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
async resolveContext() {
|
|
211
|
+
return {
|
|
212
|
+
workspace: {
|
|
213
|
+
id: 11
|
|
214
|
+
},
|
|
215
|
+
membership: {
|
|
216
|
+
roleId: "member"
|
|
217
|
+
},
|
|
218
|
+
permissions: []
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
hasPermission({ permission, permissions }) {
|
|
222
|
+
return Array.isArray(permissions) && permissions.includes(permission);
|
|
223
|
+
},
|
|
224
|
+
onPolicyDenied(event) {
|
|
225
|
+
denyEvents.push(event);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await registerPlugin(fastify);
|
|
230
|
+
await assert.rejects(
|
|
231
|
+
() =>
|
|
232
|
+
state.preHandler(
|
|
233
|
+
{
|
|
234
|
+
method: "GET",
|
|
235
|
+
raw: { url: "/api/workspace/projects" },
|
|
236
|
+
routeOptions: {
|
|
237
|
+
config: {
|
|
238
|
+
authPolicy: "required",
|
|
239
|
+
contextPolicy: "required",
|
|
240
|
+
permission: "projects.read"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{}
|
|
245
|
+
),
|
|
246
|
+
/Forbidden/
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
assert.deepEqual(denyEvents.map((event) => event.reason), ["forbidden_permission"]);
|
|
250
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
4
|
+
import { AUTH_POLICY_CONTEXT_RESOLVER_TOKEN } from "../src/server/lib/tokens.js";
|
|
5
|
+
import { FastifyAuthPolicyServiceProvider } from "../src/server/providers/FastifyAuthPolicyServiceProvider.js";
|
|
6
|
+
import { createFakeFastifyPolicyRuntime } from "../../../tooling/testUtils/fakeFastify.mjs";
|
|
7
|
+
|
|
8
|
+
test("FastifyAuthPolicyServiceProvider registers auth policy plugin through provider boot", async () => {
|
|
9
|
+
const { fastify, state } = createFakeFastifyPolicyRuntime();
|
|
10
|
+
const bag = new Map([
|
|
11
|
+
[KERNEL_TOKENS.Fastify, fastify],
|
|
12
|
+
[KERNEL_TOKENS.Env, { NODE_ENV: "test" }],
|
|
13
|
+
[KERNEL_TOKENS.Logger, console],
|
|
14
|
+
[
|
|
15
|
+
"authService",
|
|
16
|
+
{
|
|
17
|
+
async authenticateRequest() {
|
|
18
|
+
return {
|
|
19
|
+
authenticated: false,
|
|
20
|
+
actor: null,
|
|
21
|
+
transientFailure: false
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const app = {
|
|
29
|
+
has(token) {
|
|
30
|
+
return bag.has(token);
|
|
31
|
+
},
|
|
32
|
+
make(token) {
|
|
33
|
+
if (!bag.has(token)) {
|
|
34
|
+
throw new Error(`Missing token ${String(token)}`);
|
|
35
|
+
}
|
|
36
|
+
return bag.get(token);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const provider = new FastifyAuthPolicyServiceProvider();
|
|
41
|
+
provider.register(app);
|
|
42
|
+
await provider.boot(app);
|
|
43
|
+
|
|
44
|
+
assert.ok(state.requestDecorators.has("user"));
|
|
45
|
+
assert.ok(state.requestDecorators.has("workspace"));
|
|
46
|
+
assert.ok(state.requestDecorators.has("membership"));
|
|
47
|
+
assert.ok(state.requestDecorators.has("permissions"));
|
|
48
|
+
assert.equal(typeof state.preHandler, "function");
|
|
49
|
+
assert.ok(state.registeredPlugins.length >= 3);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("FastifyAuthPolicyServiceProvider wires optional auth policy context resolver", async () => {
|
|
53
|
+
const { fastify, state } = createFakeFastifyPolicyRuntime();
|
|
54
|
+
const bag = new Map([
|
|
55
|
+
[KERNEL_TOKENS.Fastify, fastify],
|
|
56
|
+
[KERNEL_TOKENS.Env, { NODE_ENV: "test" }],
|
|
57
|
+
[KERNEL_TOKENS.Logger, console],
|
|
58
|
+
[
|
|
59
|
+
"authService",
|
|
60
|
+
{
|
|
61
|
+
async authenticateRequest() {
|
|
62
|
+
return {
|
|
63
|
+
authenticated: true,
|
|
64
|
+
actor: { id: 7 },
|
|
65
|
+
transientFailure: false
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
AUTH_POLICY_CONTEXT_RESOLVER_TOKEN,
|
|
72
|
+
async ({ actor, request }) => ({
|
|
73
|
+
workspace: { id: 11, slug: String(request?.params?.workspaceSlug || "").toLowerCase() },
|
|
74
|
+
membership: { roleId: "member" },
|
|
75
|
+
permissions: actor?.id === 7 ? ["projects.read"] : []
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const app = {
|
|
81
|
+
has(token) {
|
|
82
|
+
return bag.has(token);
|
|
83
|
+
},
|
|
84
|
+
make(token) {
|
|
85
|
+
if (!bag.has(token)) {
|
|
86
|
+
throw new Error(`Missing token ${String(token)}`);
|
|
87
|
+
}
|
|
88
|
+
return bag.get(token);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const provider = new FastifyAuthPolicyServiceProvider();
|
|
93
|
+
provider.register(app);
|
|
94
|
+
await provider.boot(app);
|
|
95
|
+
|
|
96
|
+
const request = {
|
|
97
|
+
method: "GET",
|
|
98
|
+
raw: { url: "/api/w/acme/projects" },
|
|
99
|
+
params: { workspaceSlug: "ACME" },
|
|
100
|
+
routeOptions: {
|
|
101
|
+
config: {
|
|
102
|
+
authPolicy: "required",
|
|
103
|
+
contextPolicy: "required",
|
|
104
|
+
permission: "projects.read"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
await state.preHandler(request, {});
|
|
110
|
+
assert.equal(request.workspace?.id, 11);
|
|
111
|
+
assert.equal(request.workspace?.slug, "acme");
|
|
112
|
+
assert.equal(request.membership?.roleId, "member");
|
|
113
|
+
assert.deepEqual(request.permissions, ["projects.read"]);
|
|
114
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { mergeAuthPolicy, withAuthPolicy } from "../src/server/lib/index.js";
|
|
5
|
+
|
|
6
|
+
test("withAuthPolicy applies stable defaults", () => {
|
|
7
|
+
const wrapped = withAuthPolicy();
|
|
8
|
+
assert.deepEqual(wrapped, {
|
|
9
|
+
config: {
|
|
10
|
+
authPolicy: "public",
|
|
11
|
+
contextPolicy: "none",
|
|
12
|
+
surface: "",
|
|
13
|
+
permission: "",
|
|
14
|
+
ownerParam: null,
|
|
15
|
+
userField: "id",
|
|
16
|
+
ownerResolver: null,
|
|
17
|
+
csrfProtection: true
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("mergeAuthPolicy preserves existing config fields and adds auth policy defaults", () => {
|
|
23
|
+
const merged = mergeAuthPolicy(
|
|
24
|
+
{
|
|
25
|
+
method: "GET",
|
|
26
|
+
url: "/api/example",
|
|
27
|
+
config: {
|
|
28
|
+
rateLimit: {
|
|
29
|
+
max: 5,
|
|
30
|
+
timeWindow: "1 minute"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(merged.config, {
|
|
38
|
+
rateLimit: {
|
|
39
|
+
max: 5,
|
|
40
|
+
timeWindow: "1 minute"
|
|
41
|
+
},
|
|
42
|
+
authPolicy: "public",
|
|
43
|
+
contextPolicy: "none",
|
|
44
|
+
surface: "",
|
|
45
|
+
permission: "",
|
|
46
|
+
ownerParam: null,
|
|
47
|
+
userField: "id",
|
|
48
|
+
ownerResolver: null,
|
|
49
|
+
csrfProtection: true
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("mergeAuthPolicy normalizes policy metadata and keeps explicit settings", () => {
|
|
54
|
+
const resolver = () => 7;
|
|
55
|
+
const merged = mergeAuthPolicy(
|
|
56
|
+
{
|
|
57
|
+
config: {
|
|
58
|
+
authPolicy: "public",
|
|
59
|
+
contextPolicy: "none"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
authPolicy: "own",
|
|
64
|
+
contextPolicy: "required",
|
|
65
|
+
surface: "admin",
|
|
66
|
+
permission: "workspace.members.manage",
|
|
67
|
+
ownerParam: "userId",
|
|
68
|
+
userField: "id",
|
|
69
|
+
ownerResolver: resolver,
|
|
70
|
+
csrfProtection: false
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
assert.equal(merged.config.authPolicy, "own");
|
|
75
|
+
assert.equal(merged.config.contextPolicy, "required");
|
|
76
|
+
assert.equal(merged.config.surface, "admin");
|
|
77
|
+
assert.equal(merged.config.permission, "workspace.members.manage");
|
|
78
|
+
assert.equal(merged.config.ownerParam, "userId");
|
|
79
|
+
assert.equal(merged.config.userField, "id");
|
|
80
|
+
assert.equal(merged.config.ownerResolver, resolver);
|
|
81
|
+
assert.equal(merged.config.csrfProtection, false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("mergeAuthPolicy coerces unsupported owner resolver values to null", () => {
|
|
85
|
+
const merged = mergeAuthPolicy(
|
|
86
|
+
{
|
|
87
|
+
config: {
|
|
88
|
+
ownerResolver: "not-a-function"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
assert.equal(merged.config.ownerResolver, null);
|
|
95
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createAuthRouteVisibilityResolver } from "../src/server/lib/routeVisibilityResolver.js";
|
|
4
|
+
|
|
5
|
+
test("auth route visibility resolver contributes actor scope for core user visibility only", () => {
|
|
6
|
+
const resolver = createAuthRouteVisibilityResolver();
|
|
7
|
+
|
|
8
|
+
assert.deepEqual(
|
|
9
|
+
resolver.resolve({
|
|
10
|
+
visibility: "user",
|
|
11
|
+
context: {
|
|
12
|
+
actor: {
|
|
13
|
+
id: "user_7"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}),
|
|
17
|
+
{
|
|
18
|
+
userOwnerId: "user_7",
|
|
19
|
+
requiresActorScope: true
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(
|
|
24
|
+
resolver.resolve({
|
|
25
|
+
visibility: "workspace_user",
|
|
26
|
+
context: {
|
|
27
|
+
actor: {
|
|
28
|
+
id: "user_7"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}),
|
|
32
|
+
{}
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { normalizeReturnToPath } from "../src/server/utils.js";
|
|
5
|
+
|
|
6
|
+
test("normalizeReturnToPath keeps internal paths", () => {
|
|
7
|
+
assert.equal(normalizeReturnToPath("/w/acme"), "/w/acme");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("normalizeReturnToPath allows absolute urls for configured origins", () => {
|
|
11
|
+
assert.equal(
|
|
12
|
+
normalizeReturnToPath("https://app.example.com/w/acme", {
|
|
13
|
+
fallback: "/",
|
|
14
|
+
allowedOrigins: ["https://app.example.com", "https://admin.example.com"]
|
|
15
|
+
}),
|
|
16
|
+
"https://app.example.com/w/acme"
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("normalizeReturnToPath rejects absolute urls for unconfigured origins", () => {
|
|
21
|
+
assert.equal(
|
|
22
|
+
normalizeReturnToPath("https://evil.example.com/phishing", {
|
|
23
|
+
fallback: "/",
|
|
24
|
+
allowedOrigins: ["https://app.example.com"]
|
|
25
|
+
}),
|
|
26
|
+
"/"
|
|
27
|
+
);
|
|
28
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { runAuthSignOutFlow } from "../src/client/signOutFlow.js";
|
|
5
|
+
|
|
6
|
+
test("runAuthSignOutFlow executes logout then cleanup hooks", async () => {
|
|
7
|
+
const calls = [];
|
|
8
|
+
await runAuthSignOutFlow({
|
|
9
|
+
authApi: {
|
|
10
|
+
async logout() {
|
|
11
|
+
calls.push("logout");
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
clearCsrfTokenCache() {
|
|
15
|
+
calls.push("clearCsrf");
|
|
16
|
+
},
|
|
17
|
+
async afterSignOut() {
|
|
18
|
+
calls.push("afterSignOut");
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
assert.deepEqual(calls, ["logout", "clearCsrf", "afterSignOut"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("runAuthSignOutFlow runs cleanup hooks when logout fails and rethrows error", async () => {
|
|
26
|
+
const calls = [];
|
|
27
|
+
const expectedError = new Error("logout failed");
|
|
28
|
+
|
|
29
|
+
await assert.rejects(
|
|
30
|
+
() =>
|
|
31
|
+
runAuthSignOutFlow({
|
|
32
|
+
authApi: {
|
|
33
|
+
async logout() {
|
|
34
|
+
calls.push("logout");
|
|
35
|
+
throw expectedError;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
clearCsrfTokenCache() {
|
|
39
|
+
calls.push("clearCsrf");
|
|
40
|
+
},
|
|
41
|
+
async afterSignOut() {
|
|
42
|
+
calls.push("afterSignOut");
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
expectedError
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
assert.deepEqual(calls, ["logout", "clearCsrf", "afterSignOut"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("runAuthSignOutFlow validates authApi logout shape", async () => {
|
|
52
|
+
await assert.rejects(
|
|
53
|
+
() =>
|
|
54
|
+
runAuthSignOutFlow({
|
|
55
|
+
authApi: null
|
|
56
|
+
}),
|
|
57
|
+
/requires authApi/
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
await assert.rejects(
|
|
61
|
+
() =>
|
|
62
|
+
runAuthSignOutFlow({
|
|
63
|
+
authApi: {}
|
|
64
|
+
}),
|
|
65
|
+
/requires authApi\.logout/
|
|
66
|
+
);
|
|
67
|
+
});
|