@jskit-ai/auth-provider-local-core 0.1.8 → 0.1.9
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 +4 -4
- package/package.json +3 -3
- package/src/server/lib/index.js +4 -0
- package/src/server/lib/passwords.js +5 -6
- package/src/server/lib/registerHookDecorator.js +96 -0
- package/src/server/providers/AuthLocalServiceProvider.js +13 -6
- package/test/providerRuntime.test.js +407 -177
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
"packageVersion": 1,
|
|
3
3
|
"packageId": "@jskit-ai/auth-provider-local-core",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.9",
|
|
5
5
|
"kind": "runtime",
|
|
6
6
|
"description": "Local auth provider with a file backend default and no database requirement.",
|
|
7
7
|
"dependsOn": [
|
|
@@ -41,7 +41,7 @@ export default Object.freeze({
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"subpath": "./server/lib/index",
|
|
44
|
-
"summary": "Exports local auth service, file backend helpers,
|
|
44
|
+
"summary": "Exports local auth service, file backend helpers, password strategy helpers, and local auth register hook decorators."
|
|
45
45
|
}
|
|
46
46
|
],
|
|
47
47
|
"containerTokens": {
|
|
@@ -57,8 +57,8 @@ export default Object.freeze({
|
|
|
57
57
|
"mutations": {
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"runtime": {
|
|
60
|
-
"@jskit-ai/auth-core": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/auth-core": "0.1.108",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.110",
|
|
62
62
|
"nodemailer": "^7.0.10",
|
|
63
63
|
"dotenv": "^16.4.5"
|
|
64
64
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/auth-provider-local-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"./server/lib/index": "./src/server/lib/index.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@jskit-ai/auth-core": "0.1.
|
|
15
|
-
"@jskit-ai/kernel": "0.1.
|
|
14
|
+
"@jskit-ai/auth-core": "0.1.108",
|
|
15
|
+
"@jskit-ai/kernel": "0.1.110",
|
|
16
16
|
"nodemailer": "^7.0.10"
|
|
17
17
|
}
|
|
18
18
|
}
|
package/src/server/lib/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { createLocalAuthService } from "./service.js";
|
|
2
2
|
export { createLocalFileBackend } from "./fileBackend.js";
|
|
3
|
+
export {
|
|
4
|
+
LOCAL_AUTH_USER_REGISTERED_EVENT,
|
|
5
|
+
createLocalAuthRegisterHookDecorator
|
|
6
|
+
} from "./registerHookDecorator.js";
|
|
3
7
|
export { hashPassword, normalizePasswordStrategy, verifyPassword } from "./passwords.js";
|
|
@@ -42,7 +42,7 @@ async function verifyPassword(password, record) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function normalizePasswordStrategy(strategy = null) {
|
|
45
|
-
if (
|
|
45
|
+
if (strategy == null) {
|
|
46
46
|
return Object.freeze({
|
|
47
47
|
hashPassword,
|
|
48
48
|
verifyPassword
|
|
@@ -53,13 +53,12 @@ function normalizePasswordStrategy(strategy = null) {
|
|
|
53
53
|
throw new TypeError("Local auth password strategy must be an object.");
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
const strategyHashPassword = typeof normalized.hashPassword === "undefined"
|
|
56
|
+
const strategyHashPassword = typeof strategy.hashPassword === "undefined"
|
|
58
57
|
? hashPassword
|
|
59
|
-
:
|
|
60
|
-
const strategyVerifyPassword = typeof
|
|
58
|
+
: strategy.hashPassword;
|
|
59
|
+
const strategyVerifyPassword = typeof strategy.verifyPassword === "undefined"
|
|
61
60
|
? verifyPassword
|
|
62
|
-
:
|
|
61
|
+
: strategy.verifyPassword;
|
|
63
62
|
|
|
64
63
|
if (typeof strategyHashPassword !== "function") {
|
|
65
64
|
throw new TypeError("Local auth password strategy hashPassword must be a function.");
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const LOCAL_AUTH_USER_REGISTERED_EVENT = "auth.local.user.registered";
|
|
2
|
+
|
|
3
|
+
function assertBlockingMode(hook) {
|
|
4
|
+
if (typeof hook?.blocking !== "boolean") {
|
|
5
|
+
throw new TypeError("Local auth register hooks must set blocking to true or false.");
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeRegisterHook(hook = null) {
|
|
10
|
+
if (!hook || typeof hook !== "object" || Array.isArray(hook)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
assertBlockingMode(hook);
|
|
14
|
+
if (typeof hook.handle !== "function") {
|
|
15
|
+
throw new TypeError("Local auth register hook handle must be a function.");
|
|
16
|
+
}
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
hookId: String(hook.hookId || "anonymous"),
|
|
19
|
+
blocking: hook.blocking,
|
|
20
|
+
handle: hook.handle
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function logNonBlockingHookFailure({ logger, hook, error } = {}) {
|
|
25
|
+
const payload = {
|
|
26
|
+
err: error,
|
|
27
|
+
hook: hook?.hookId || "anonymous",
|
|
28
|
+
event: LOCAL_AUTH_USER_REGISTERED_EVENT
|
|
29
|
+
};
|
|
30
|
+
if (logger && typeof logger.warn === "function") {
|
|
31
|
+
logger.warn(payload, "Non-blocking local auth register hook failed.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.warn("Non-blocking local auth register hook failed.", payload);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function runRegisterHook({ hook, result, logger }) {
|
|
38
|
+
if (!hook) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const payload = Object.freeze({
|
|
43
|
+
event: LOCAL_AUTH_USER_REGISTERED_EVENT,
|
|
44
|
+
actor: result?.actor || null,
|
|
45
|
+
profile: result?.profile || null,
|
|
46
|
+
authResult: result
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (hook.blocking) {
|
|
50
|
+
await hook.handle(payload);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Promise.resolve()
|
|
55
|
+
.then(() => hook.handle(payload))
|
|
56
|
+
.catch((error) => logNonBlockingHookFailure({ logger, hook, error }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createLocalAuthRegisterHookDecorator({
|
|
60
|
+
decoratorId = "auth.local.registerHook",
|
|
61
|
+
order = 0,
|
|
62
|
+
logger = null,
|
|
63
|
+
hook = null
|
|
64
|
+
} = {}) {
|
|
65
|
+
const registerHook = normalizeRegisterHook(hook);
|
|
66
|
+
|
|
67
|
+
if (!registerHook) {
|
|
68
|
+
throw new TypeError("createLocalAuthRegisterHookDecorator requires a hook object.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
decoratorId: String(decoratorId || registerHook.hookId || "auth.local.registerHook"),
|
|
73
|
+
order: Number.isFinite(order) ? Number(order) : 0,
|
|
74
|
+
decorateAuthService(authService) {
|
|
75
|
+
if (!authService || typeof authService.register !== "function") {
|
|
76
|
+
return authService;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return Object.freeze(
|
|
80
|
+
Object.defineProperty(Object.create(authService), "register", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
value: async function register(input = {}) {
|
|
83
|
+
const result = await authService.register(input);
|
|
84
|
+
await runRegisterHook({ hook: registerHook, result, logger });
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
LOCAL_AUTH_USER_REGISTERED_EVENT,
|
|
95
|
+
createLocalAuthRegisterHookDecorator
|
|
96
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyAuthServiceDecorators } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -6,6 +7,11 @@ import { createLocalFileBackend } from "../lib/fileBackend.js";
|
|
|
6
7
|
|
|
7
8
|
const DEFAULT_STORE_DIR = ".jskit/auth";
|
|
8
9
|
|
|
10
|
+
function resolveLocalBackendMode(scope) {
|
|
11
|
+
const env = resolveRuntimeEnv(scope);
|
|
12
|
+
return String(env.AUTH_LOCAL_BACKEND || "file").trim().toLowerCase() || "file";
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
function parseBoolean(value, fallback = false) {
|
|
10
16
|
const raw = String(value || "").trim().toLowerCase();
|
|
11
17
|
if (!raw) {
|
|
@@ -157,12 +163,9 @@ class AuthLocalServiceProvider {
|
|
|
157
163
|
throw new Error("AuthLocalServiceProvider cannot register authService because another auth provider already registered it.");
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
if (!app.has("auth.local.backend")) {
|
|
166
|
+
if (!app.has("auth.local.backend") && resolveLocalBackendMode(app) === "file") {
|
|
161
167
|
app.singleton("auth.local.backend", (scope) => {
|
|
162
168
|
const config = resolveConfig(scope);
|
|
163
|
-
if (config.backend !== "file") {
|
|
164
|
-
throw new Error(`AUTH_LOCAL_BACKEND="${config.backend}" requires a custom auth.local.backend provider.`);
|
|
165
|
-
}
|
|
166
169
|
return createLocalFileBackend({
|
|
167
170
|
storeDir: config.storeDir
|
|
168
171
|
});
|
|
@@ -171,6 +174,9 @@ class AuthLocalServiceProvider {
|
|
|
171
174
|
|
|
172
175
|
app.singleton("authService", (scope) => {
|
|
173
176
|
const config = resolveConfig(scope);
|
|
177
|
+
if (!scope.has("auth.local.backend")) {
|
|
178
|
+
throw new Error(`AUTH_LOCAL_BACKEND="${config.backend}" requires a package or app provider that registers auth.local.backend.`);
|
|
179
|
+
}
|
|
174
180
|
const backend = scope.make("auth.local.backend");
|
|
175
181
|
const profileProjector = scope.has("auth.profile.projector")
|
|
176
182
|
? {
|
|
@@ -200,13 +206,14 @@ class AuthLocalServiceProvider {
|
|
|
200
206
|
const passwordStrategy = scope.has("auth.local.passwordStrategy")
|
|
201
207
|
? scope.make("auth.local.passwordStrategy")
|
|
202
208
|
: null;
|
|
203
|
-
|
|
209
|
+
const authService = createLocalAuthService({
|
|
204
210
|
backend,
|
|
205
211
|
config,
|
|
206
212
|
profileProjector,
|
|
207
213
|
passwordStrategy,
|
|
208
214
|
invitationContextResolver
|
|
209
215
|
});
|
|
216
|
+
return applyAuthServiceDecorators(scope, authService);
|
|
210
217
|
});
|
|
211
218
|
}
|
|
212
219
|
|
|
@@ -218,4 +225,4 @@ class AuthLocalServiceProvider {
|
|
|
218
225
|
}
|
|
219
226
|
}
|
|
220
227
|
|
|
221
|
-
export { AuthLocalServiceProvider };
|
|
228
|
+
export { AuthLocalServiceProvider, resolveLocalBackendMode };
|
|
@@ -5,10 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { createApplication } from "@jskit-ai/kernel/_testable";
|
|
7
7
|
import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
|
|
8
|
+
import { registerAuthServiceDecorator } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
|
|
8
9
|
import { AuthActionsServiceProvider } from "@jskit-ai/auth-core/server/providers/AuthActionsServiceProvider";
|
|
9
10
|
import { AuthLocalServiceProvider } from "../src/server/providers/AuthLocalServiceProvider.js";
|
|
10
11
|
import { AuthProviderServiceProvider } from "../src/server/providers/AuthProviderServiceProvider.js";
|
|
11
12
|
import {
|
|
13
|
+
createLocalAuthRegisterHookDecorator,
|
|
12
14
|
createLocalAuthService,
|
|
13
15
|
createLocalFileBackend,
|
|
14
16
|
hashPassword,
|
|
@@ -54,7 +56,9 @@ async function createStartedApp({
|
|
|
54
56
|
profileProjector = null,
|
|
55
57
|
profileProjectorFactory = null,
|
|
56
58
|
passwordStrategy = null,
|
|
57
|
-
invitationContextResolver = null
|
|
59
|
+
invitationContextResolver = null,
|
|
60
|
+
logger = null,
|
|
61
|
+
configureApp = null
|
|
58
62
|
} = {}) {
|
|
59
63
|
const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-"));
|
|
60
64
|
const app = createApplication();
|
|
@@ -66,12 +70,15 @@ async function createStartedApp({
|
|
|
66
70
|
APP_PUBLIC_URL: "http://localhost:5173",
|
|
67
71
|
NODE_ENV: "test"
|
|
68
72
|
});
|
|
69
|
-
app.instance(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
app.instance(
|
|
74
|
+
"jskit.logger",
|
|
75
|
+
logger || {
|
|
76
|
+
info() {},
|
|
77
|
+
warn() {},
|
|
78
|
+
error() {},
|
|
79
|
+
debug() {}
|
|
80
|
+
}
|
|
81
|
+
);
|
|
75
82
|
app.instance("domainEvents", {
|
|
76
83
|
async publish() {}
|
|
77
84
|
});
|
|
@@ -87,6 +94,9 @@ async function createStartedApp({
|
|
|
87
94
|
if (invitationContextResolver) {
|
|
88
95
|
app.instance("auth.invitationContextResolver", invitationContextResolver);
|
|
89
96
|
}
|
|
97
|
+
if (typeof configureApp === "function") {
|
|
98
|
+
configureApp(app);
|
|
99
|
+
}
|
|
90
100
|
await app.start({
|
|
91
101
|
providers: [
|
|
92
102
|
ActionRuntimeServiceProvider,
|
|
@@ -152,6 +162,102 @@ test("local auth provider registers, logs in, reads session, and logs out with f
|
|
|
152
162
|
assert.match(usersFile, /^user:v1:/);
|
|
153
163
|
});
|
|
154
164
|
|
|
165
|
+
test("local auth provider applies auth service decorators for blocking and non-blocking hooks", async () => {
|
|
166
|
+
const calls = [];
|
|
167
|
+
const errors = [];
|
|
168
|
+
const { app } = await createStartedApp({
|
|
169
|
+
logger: {
|
|
170
|
+
info() {},
|
|
171
|
+
warn(payload, message) {
|
|
172
|
+
errors.push({ payload, message });
|
|
173
|
+
},
|
|
174
|
+
error() {},
|
|
175
|
+
debug() {}
|
|
176
|
+
},
|
|
177
|
+
configureApp(app) {
|
|
178
|
+
registerAuthServiceDecorator(app, "test.auth.local.nonBlockingHook", (scope) => ({
|
|
179
|
+
...createLocalAuthRegisterHookDecorator({
|
|
180
|
+
decoratorId: "test.auth.local.nonBlockingHook",
|
|
181
|
+
order: 20,
|
|
182
|
+
logger: scope.make("jskit.logger"),
|
|
183
|
+
hook: {
|
|
184
|
+
hookId: "audit",
|
|
185
|
+
blocking: false,
|
|
186
|
+
async handle({ actor }) {
|
|
187
|
+
calls.push({ hook: "audit", email: actor.email });
|
|
188
|
+
throw new Error("audit unavailable");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
}));
|
|
193
|
+
registerAuthServiceDecorator(app, "test.auth.local.blockingHook", () =>
|
|
194
|
+
createLocalAuthRegisterHookDecorator({
|
|
195
|
+
decoratorId: "test.auth.local.blockingHook",
|
|
196
|
+
order: 10,
|
|
197
|
+
hook: {
|
|
198
|
+
hookId: "permissions",
|
|
199
|
+
blocking: true,
|
|
200
|
+
async handle({ actor, profile }) {
|
|
201
|
+
calls.push({ hook: "permissions", email: actor.email });
|
|
202
|
+
if (profile?.displayName === "Block Permissions") {
|
|
203
|
+
throw new Error("permission provisioning failed");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
const authService = app.make("authService");
|
|
212
|
+
|
|
213
|
+
const registered = await authService.register({
|
|
214
|
+
email: "hooks@example.com",
|
|
215
|
+
password: "correct horse battery staple",
|
|
216
|
+
displayName: "Hooks"
|
|
217
|
+
});
|
|
218
|
+
assert.equal(registered.actor.email, "hooks@example.com");
|
|
219
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
220
|
+
assert.deepEqual(calls, [
|
|
221
|
+
{ hook: "permissions", email: "hooks@example.com" },
|
|
222
|
+
{ hook: "audit", email: "hooks@example.com" }
|
|
223
|
+
]);
|
|
224
|
+
assert.equal(errors.length, 1);
|
|
225
|
+
assert.equal(errors[0].message, "Non-blocking local auth register hook failed.");
|
|
226
|
+
|
|
227
|
+
await assert.rejects(
|
|
228
|
+
() =>
|
|
229
|
+
authService.register({
|
|
230
|
+
email: "blocked-hooks@example.com",
|
|
231
|
+
password: "correct horse battery staple",
|
|
232
|
+
displayName: "Block Permissions"
|
|
233
|
+
}),
|
|
234
|
+
/permission provisioning failed/
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("local auth register hook decorators require explicit blocking mode", () => {
|
|
239
|
+
assert.throws(
|
|
240
|
+
() =>
|
|
241
|
+
createLocalAuthRegisterHookDecorator({
|
|
242
|
+
hook: {
|
|
243
|
+
hookId: "invalid",
|
|
244
|
+
async handle() {}
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
/blocking to true or false/
|
|
248
|
+
);
|
|
249
|
+
assert.throws(
|
|
250
|
+
() =>
|
|
251
|
+
createLocalAuthRegisterHookDecorator({
|
|
252
|
+
hook: {
|
|
253
|
+
hookId: "invalid",
|
|
254
|
+
blocking: false
|
|
255
|
+
}
|
|
256
|
+
}),
|
|
257
|
+
/handle must be a function/
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
155
261
|
test("local auth provider resolves a custom password strategy from the container", async () => {
|
|
156
262
|
const passwordStrategy = {
|
|
157
263
|
prefix: "strategy",
|
|
@@ -196,10 +302,26 @@ test("local auth password strategy supports partial overrides and rejects invali
|
|
|
196
302
|
assert.equal(await verifyPassword("new password value", defaultHashed), true);
|
|
197
303
|
assert.equal(await strategy.verifyPassword("old password value", "legacy:users:old password value"), true);
|
|
198
304
|
|
|
305
|
+
const hashOnlyStrategy = normalizePasswordStrategy({
|
|
306
|
+
async hashPassword(password) {
|
|
307
|
+
return hashPassword(password);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
const hashOnlyRecord = await hashOnlyStrategy.hashPassword("hash only password");
|
|
311
|
+
assert.equal(await hashOnlyStrategy.verifyPassword("hash only password", hashOnlyRecord), true);
|
|
312
|
+
|
|
199
313
|
assert.throws(
|
|
200
314
|
() => normalizePasswordStrategy("invalid"),
|
|
201
315
|
/Local auth password strategy must be an object/
|
|
202
316
|
);
|
|
317
|
+
assert.throws(
|
|
318
|
+
() => normalizePasswordStrategy(false),
|
|
319
|
+
/Local auth password strategy must be an object/
|
|
320
|
+
);
|
|
321
|
+
assert.throws(
|
|
322
|
+
() => normalizePasswordStrategy(0),
|
|
323
|
+
/Local auth password strategy must be an object/
|
|
324
|
+
);
|
|
203
325
|
assert.throws(
|
|
204
326
|
() => normalizePasswordStrategy([]),
|
|
205
327
|
/Local auth password strategy must be an object/
|
|
@@ -222,165 +344,280 @@ test("local auth login verifies password and creates session in one backend tran
|
|
|
222
344
|
const password = await hashPassword("current password value");
|
|
223
345
|
let transactions = 0;
|
|
224
346
|
let sessionCreated = false;
|
|
225
|
-
const backend = {
|
|
347
|
+
const backend = createLocalFileBackend({
|
|
348
|
+
storeDir: await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-login-tx-"))
|
|
349
|
+
});
|
|
350
|
+
await backend.withTransaction((tx) =>
|
|
351
|
+
tx.users.create({
|
|
352
|
+
id: "usr_login_tx",
|
|
353
|
+
email: "tx@example.com",
|
|
354
|
+
displayName: "Transaction User",
|
|
355
|
+
password
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
const wrappedBackend = {
|
|
226
359
|
async withTransaction(callback) {
|
|
227
360
|
transactions += 1;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
disabled: false
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
sessions: {
|
|
242
|
-
async create(input) {
|
|
243
|
-
sessionCreated = true;
|
|
244
|
-
return {
|
|
245
|
-
...input,
|
|
246
|
-
createdAt: new Date().toISOString(),
|
|
247
|
-
revokedAt: ""
|
|
248
|
-
};
|
|
361
|
+
return backend.withTransaction(async (tx) => {
|
|
362
|
+
const result = await callback({
|
|
363
|
+
...tx,
|
|
364
|
+
sessions: {
|
|
365
|
+
...tx.sessions,
|
|
366
|
+
async create(input) {
|
|
367
|
+
sessionCreated = true;
|
|
368
|
+
return tx.sessions.create(input);
|
|
369
|
+
}
|
|
249
370
|
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
371
|
+
});
|
|
372
|
+
return result;
|
|
373
|
+
});
|
|
253
374
|
}
|
|
254
375
|
};
|
|
255
376
|
const authService = createLocalAuthService({
|
|
256
|
-
backend,
|
|
377
|
+
backend: wrappedBackend,
|
|
257
378
|
config: {
|
|
258
|
-
nodeEnv: "test",
|
|
259
379
|
sessionSecret: "test-secret",
|
|
260
|
-
|
|
380
|
+
nodeEnv: "test",
|
|
261
381
|
smtpConfigured: false,
|
|
262
|
-
recoveryDevOutput: "
|
|
382
|
+
recoveryDevOutput: "response",
|
|
383
|
+
appPublicUrl: "http://localhost:5173"
|
|
263
384
|
}
|
|
264
385
|
});
|
|
265
386
|
|
|
266
387
|
const result = await authService.login({
|
|
267
|
-
email: "
|
|
388
|
+
email: "tx@example.com",
|
|
268
389
|
password: "current password value"
|
|
269
390
|
});
|
|
270
391
|
|
|
271
|
-
assert.equal(result.actor.email, "
|
|
272
|
-
assert.equal(Boolean(result.session?.access_token), true);
|
|
273
|
-
assert.equal(sessionCreated, true);
|
|
392
|
+
assert.equal(result.actor.email, "tx@example.com");
|
|
274
393
|
assert.equal(transactions, 1);
|
|
394
|
+
assert.equal(sessionCreated, true);
|
|
275
395
|
});
|
|
276
396
|
|
|
277
397
|
test("local auth service accepts a custom strategy for legacy stored password records", async () => {
|
|
278
|
-
const
|
|
279
|
-
[
|
|
280
|
-
"legacy@example.com",
|
|
281
|
-
{
|
|
282
|
-
id: "usr_legacy",
|
|
283
|
-
email: "legacy@example.com",
|
|
284
|
-
displayName: "Legacy User",
|
|
285
|
-
password: {
|
|
286
|
-
algorithm: "legacy-bcrypt",
|
|
287
|
-
hash: "legacy password value"
|
|
288
|
-
},
|
|
289
|
-
disabled: false
|
|
290
|
-
}
|
|
291
|
-
]
|
|
292
|
-
]);
|
|
398
|
+
const passwordRecords = [];
|
|
293
399
|
const sessions = [];
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
400
|
+
const authService = createLocalAuthService({
|
|
401
|
+
backend: {
|
|
402
|
+
async withTransaction(callback) {
|
|
403
|
+
return callback({
|
|
404
|
+
users: {
|
|
405
|
+
async findByEmail(email) {
|
|
406
|
+
if (email !== "legacy@example.com") {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
id: "usr_legacy",
|
|
411
|
+
email,
|
|
412
|
+
displayName: "Legacy User",
|
|
413
|
+
password: "legacy:legacy@example.com:old-password",
|
|
414
|
+
disabled: false
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
async findById(userId) {
|
|
418
|
+
if (userId !== "usr_legacy") {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
id: "usr_legacy",
|
|
423
|
+
email: "legacy@example.com",
|
|
424
|
+
displayName: "Legacy User",
|
|
425
|
+
password: passwordRecords.at(-1) || "legacy:legacy@example.com:old-password",
|
|
426
|
+
disabled: false
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
async create(input) {
|
|
430
|
+
passwordRecords.push(input.password);
|
|
431
|
+
return input;
|
|
432
|
+
},
|
|
433
|
+
async updatePassword(_userId, password) {
|
|
434
|
+
passwordRecords.push(password);
|
|
435
|
+
return {
|
|
436
|
+
id: "usr_legacy",
|
|
437
|
+
email: "legacy@example.com",
|
|
438
|
+
displayName: "Legacy User",
|
|
439
|
+
password,
|
|
440
|
+
disabled: false
|
|
441
|
+
};
|
|
442
|
+
},
|
|
443
|
+
async updateProfile() {
|
|
444
|
+
throw new Error("not needed");
|
|
445
|
+
}
|
|
300
446
|
},
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
447
|
+
sessions: {
|
|
448
|
+
async create(input) {
|
|
449
|
+
sessions.push(input);
|
|
450
|
+
return {
|
|
451
|
+
...input,
|
|
452
|
+
createdAt: new Date().toISOString(),
|
|
453
|
+
revokedAt: ""
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
async findById() {
|
|
457
|
+
return null;
|
|
458
|
+
},
|
|
459
|
+
async findByTokenHash() {
|
|
460
|
+
return null;
|
|
461
|
+
},
|
|
462
|
+
async revoke() {
|
|
463
|
+
return null;
|
|
464
|
+
},
|
|
465
|
+
async revokeForUser() {
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
recovery: {
|
|
470
|
+
async create() {},
|
|
471
|
+
async findByTokenHash() {
|
|
472
|
+
return null;
|
|
473
|
+
},
|
|
474
|
+
async consume() {
|
|
475
|
+
return null;
|
|
476
|
+
},
|
|
477
|
+
async consumeForUser() {
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
319
480
|
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
config: {
|
|
485
|
+
sessionSecret: "test-secret",
|
|
486
|
+
nodeEnv: "test",
|
|
487
|
+
smtpConfigured: false,
|
|
488
|
+
recoveryDevOutput: "response",
|
|
489
|
+
appPublicUrl: "http://localhost:5173"
|
|
490
|
+
},
|
|
491
|
+
passwordStrategy: {
|
|
492
|
+
async verifyPassword(password, record) {
|
|
493
|
+
return record === `legacy:legacy@example.com:${password}`;
|
|
494
|
+
}
|
|
323
495
|
}
|
|
324
|
-
};
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const legacyLogin = await authService.login({
|
|
499
|
+
email: "legacy@example.com",
|
|
500
|
+
password: "old-password"
|
|
501
|
+
});
|
|
502
|
+
assert.equal(legacyLogin.actor.providerUserId, "usr_legacy");
|
|
503
|
+
assert.equal(sessions.length, 1);
|
|
504
|
+
|
|
505
|
+
await authService.register({
|
|
506
|
+
email: "new@example.com",
|
|
507
|
+
password: "new-password",
|
|
508
|
+
displayName: "New User"
|
|
509
|
+
});
|
|
510
|
+
assert.equal(passwordRecords.length, 1);
|
|
511
|
+
assert.equal(passwordRecords[0].algorithm, "scrypt");
|
|
512
|
+
assert.equal(sessions.length, 2);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("local auth password strategy handles register, reset, login, and change password paths", async () => {
|
|
516
|
+
const hashCalls = [];
|
|
517
|
+
const verifyCalls = [];
|
|
518
|
+
const encodePassword = (password) => Buffer.from(String(password), "utf8").toString("base64url");
|
|
325
519
|
const passwordStrategy = {
|
|
326
520
|
async hashPassword(password) {
|
|
521
|
+
hashCalls.push(password);
|
|
327
522
|
return {
|
|
328
|
-
algorithm: "
|
|
523
|
+
algorithm: "strategy-password",
|
|
329
524
|
version: "v1",
|
|
330
|
-
salt: "
|
|
331
|
-
hash:
|
|
525
|
+
salt: "",
|
|
526
|
+
hash: encodePassword(password)
|
|
332
527
|
};
|
|
333
528
|
},
|
|
334
529
|
async verifyPassword(password, record) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
530
|
+
verifyCalls.push({
|
|
531
|
+
password,
|
|
532
|
+
hash: record?.hash || ""
|
|
533
|
+
});
|
|
534
|
+
return (
|
|
535
|
+
record?.algorithm === "strategy-password" &&
|
|
536
|
+
record.hash === encodePassword(password)
|
|
537
|
+
);
|
|
339
538
|
}
|
|
340
539
|
};
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
540
|
+
const { app } = await createStartedApp({ passwordStrategy });
|
|
541
|
+
const authService = app.make("authService");
|
|
542
|
+
|
|
543
|
+
await authService.register({
|
|
544
|
+
email: "strategy-paths@example.com",
|
|
545
|
+
password: "original password value",
|
|
546
|
+
displayName: "Strategy Paths"
|
|
547
|
+
});
|
|
548
|
+
await authService.login({
|
|
549
|
+
email: "strategy-paths@example.com",
|
|
550
|
+
password: "original password value"
|
|
351
551
|
});
|
|
352
552
|
|
|
353
|
-
const
|
|
354
|
-
email: "
|
|
355
|
-
password: "legacy password value"
|
|
553
|
+
const resetRequest = await authService.requestPasswordReset({
|
|
554
|
+
email: "strategy-paths@example.com"
|
|
356
555
|
});
|
|
357
|
-
|
|
556
|
+
const recoveryToken = new URL(resetRequest.recoveryUrl).searchParams.get("token");
|
|
557
|
+
const recovery = await authService.completePasswordRecovery({
|
|
558
|
+
code: recoveryToken,
|
|
559
|
+
type: "recovery"
|
|
560
|
+
});
|
|
561
|
+
const recoveryReply = createReplyFixture();
|
|
562
|
+
authService.writeSessionCookies(recoveryReply, recovery.session);
|
|
563
|
+
await authService.resetPassword(
|
|
564
|
+
{
|
|
565
|
+
cookies: recoveryReply.cookies
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
password: "reset password value"
|
|
569
|
+
}
|
|
570
|
+
);
|
|
358
571
|
|
|
359
|
-
await authService.
|
|
360
|
-
email: "
|
|
361
|
-
password: "
|
|
362
|
-
displayName: "New Strategy"
|
|
572
|
+
const resetLogin = await authService.login({
|
|
573
|
+
email: "strategy-paths@example.com",
|
|
574
|
+
password: "reset password value"
|
|
363
575
|
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
576
|
+
const normalReply = createReplyFixture();
|
|
577
|
+
authService.writeSessionCookies(normalReply, resetLogin.session);
|
|
578
|
+
await authService.changePassword(
|
|
579
|
+
{
|
|
580
|
+
cookies: normalReply.cookies
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
currentPassword: "reset password value",
|
|
584
|
+
newPassword: "changed password value"
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
await authService.login({
|
|
588
|
+
email: "strategy-paths@example.com",
|
|
589
|
+
password: "changed password value"
|
|
369
590
|
});
|
|
370
|
-
|
|
591
|
+
|
|
592
|
+
assert.deepEqual(hashCalls, [
|
|
593
|
+
"original password value",
|
|
594
|
+
"reset password value",
|
|
595
|
+
"changed password value"
|
|
596
|
+
]);
|
|
597
|
+
assert.deepEqual(verifyCalls.map((call) => call.password), [
|
|
598
|
+
"original password value",
|
|
599
|
+
"reset password value",
|
|
600
|
+
"reset password value",
|
|
601
|
+
"changed password value"
|
|
602
|
+
]);
|
|
371
603
|
});
|
|
372
604
|
|
|
373
605
|
test("local auth provider completes recovery through a recovery-scoped session", async () => {
|
|
374
606
|
const { app } = await createStartedApp();
|
|
375
607
|
const authService = app.make("authService");
|
|
376
|
-
|
|
377
|
-
email: "
|
|
378
|
-
password: "
|
|
379
|
-
displayName: "
|
|
608
|
+
await authService.register({
|
|
609
|
+
email: "Recovery@example.com",
|
|
610
|
+
password: "correct horse battery staple",
|
|
611
|
+
displayName: "Recovery User"
|
|
380
612
|
});
|
|
381
|
-
const normalReply = createReplyFixture();
|
|
382
|
-
authService.writeSessionCookies(normalReply, registered.session);
|
|
383
613
|
|
|
614
|
+
const normalReply = createReplyFixture();
|
|
615
|
+
const normalLogin = await authService.login({
|
|
616
|
+
email: "recovery@example.com",
|
|
617
|
+
password: "correct horse battery staple"
|
|
618
|
+
});
|
|
619
|
+
authService.writeSessionCookies(normalReply, normalLogin.session);
|
|
620
|
+
normalReply.cookies.jskit_local_recovery_token = normalReply.cookies.jskit_local_access_token;
|
|
384
621
|
await assert.rejects(
|
|
385
622
|
() =>
|
|
386
623
|
authService.resetPassword(
|
|
@@ -388,28 +625,27 @@ test("local auth provider completes recovery through a recovery-scoped session",
|
|
|
388
625
|
cookies: normalReply.cookies
|
|
389
626
|
},
|
|
390
627
|
{
|
|
391
|
-
password: "
|
|
628
|
+
password: "not allowed value"
|
|
392
629
|
}
|
|
393
630
|
),
|
|
394
|
-
/
|
|
631
|
+
/Recovery session required/
|
|
395
632
|
);
|
|
396
633
|
|
|
397
634
|
const resetRequest = await authService.requestPasswordReset({
|
|
398
|
-
email: "
|
|
635
|
+
email: "recovery@example.com"
|
|
399
636
|
});
|
|
400
|
-
assert.
|
|
401
|
-
|
|
402
|
-
const recoveryToken = new URL(resetRequest.recoveryUrl).searchParams.get("token");
|
|
403
|
-
|
|
637
|
+
assert.match(resetRequest.recoveryUrl, /^http:\/\/localhost:5173\/auth\/reset-password\?/);
|
|
638
|
+
const token = new URL(resetRequest.recoveryUrl).searchParams.get("token");
|
|
404
639
|
const recovery = await authService.completePasswordRecovery({
|
|
405
|
-
code:
|
|
640
|
+
code: token,
|
|
406
641
|
type: "recovery"
|
|
407
642
|
});
|
|
643
|
+
assert.equal(recovery.actor.email, "recovery@example.com");
|
|
644
|
+
|
|
408
645
|
const reply = createReplyFixture();
|
|
409
646
|
authService.writeSessionCookies(reply, recovery.session);
|
|
410
647
|
assert.equal(Boolean(reply.cookies.jskit_local_recovery_token), true);
|
|
411
|
-
assert.equal(reply.
|
|
412
|
-
assert.equal(reply.cookieOptions.jskit_local_refresh_token.maxAge, 15 * 60);
|
|
648
|
+
assert.equal(Boolean(reply.cookies.jskit_local_refresh_token), true);
|
|
413
649
|
|
|
414
650
|
const generalSession = await authService.authenticateRequest({ cookies: reply.cookies });
|
|
415
651
|
assert.equal(generalSession.authenticated, false);
|
|
@@ -420,39 +656,39 @@ test("local auth provider completes recovery through a recovery-scoped session",
|
|
|
420
656
|
cookies: reply.cookies
|
|
421
657
|
},
|
|
422
658
|
{
|
|
423
|
-
password: "
|
|
659
|
+
password: "updated password value"
|
|
424
660
|
}
|
|
425
661
|
);
|
|
426
662
|
|
|
427
663
|
await assert.rejects(
|
|
428
664
|
() =>
|
|
429
665
|
authService.login({
|
|
430
|
-
email: "
|
|
431
|
-
password: "
|
|
666
|
+
email: "recovery@example.com",
|
|
667
|
+
password: "correct horse battery staple"
|
|
432
668
|
}),
|
|
433
669
|
/Invalid email or password/
|
|
434
670
|
);
|
|
435
671
|
const login = await authService.login({
|
|
436
|
-
email: "
|
|
437
|
-
password: "
|
|
672
|
+
email: "recovery@example.com",
|
|
673
|
+
password: "updated password value"
|
|
438
674
|
});
|
|
439
|
-
assert.equal(login.actor.email, "
|
|
675
|
+
assert.equal(login.actor.email, "recovery@example.com");
|
|
440
676
|
});
|
|
441
677
|
|
|
442
678
|
test("local auth reset invalidates other outstanding recovery tokens for the user", async () => {
|
|
443
679
|
const { app } = await createStartedApp();
|
|
444
680
|
const authService = app.make("authService");
|
|
445
681
|
await authService.register({
|
|
446
|
-
email: "
|
|
447
|
-
password: "
|
|
448
|
-
displayName: "
|
|
682
|
+
email: "recovery-tokens@example.com",
|
|
683
|
+
password: "correct horse battery staple",
|
|
684
|
+
displayName: "Recovery Tokens"
|
|
449
685
|
});
|
|
450
686
|
|
|
451
687
|
const firstResetRequest = await authService.requestPasswordReset({
|
|
452
|
-
email: "
|
|
688
|
+
email: "recovery-tokens@example.com"
|
|
453
689
|
});
|
|
454
690
|
const secondResetRequest = await authService.requestPasswordReset({
|
|
455
|
-
email: "
|
|
691
|
+
email: "recovery-tokens@example.com"
|
|
456
692
|
});
|
|
457
693
|
const firstToken = new URL(firstResetRequest.recoveryUrl).searchParams.get("token");
|
|
458
694
|
const secondToken = new URL(secondResetRequest.recoveryUrl).searchParams.get("token");
|
|
@@ -463,13 +699,12 @@ test("local auth reset invalidates other outstanding recovery tokens for the use
|
|
|
463
699
|
});
|
|
464
700
|
const reply = createReplyFixture();
|
|
465
701
|
authService.writeSessionCookies(reply, recovery.session);
|
|
466
|
-
|
|
467
702
|
await authService.resetPassword(
|
|
468
703
|
{
|
|
469
704
|
cookies: reply.cookies
|
|
470
705
|
},
|
|
471
706
|
{
|
|
472
|
-
password: "
|
|
707
|
+
password: "updated password value"
|
|
473
708
|
}
|
|
474
709
|
);
|
|
475
710
|
|
|
@@ -487,9 +722,9 @@ test("local auth provider changes password through the account-security contract
|
|
|
487
722
|
const { app } = await createStartedApp();
|
|
488
723
|
const authService = app.make("authService");
|
|
489
724
|
const registered = await authService.register({
|
|
490
|
-
email: "
|
|
491
|
-
password: "
|
|
492
|
-
displayName: "
|
|
725
|
+
email: "Security@example.com",
|
|
726
|
+
password: "correct horse battery staple",
|
|
727
|
+
displayName: "Security User"
|
|
493
728
|
});
|
|
494
729
|
const reply = createReplyFixture();
|
|
495
730
|
authService.writeSessionCookies(reply, registered.session);
|
|
@@ -497,74 +732,69 @@ test("local auth provider changes password through the account-security contract
|
|
|
497
732
|
await assert.rejects(
|
|
498
733
|
() =>
|
|
499
734
|
authService.changePassword(
|
|
500
|
-
{ cookies: reply.cookies },
|
|
501
735
|
{
|
|
502
|
-
|
|
503
|
-
|
|
736
|
+
cookies: reply.cookies
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
currentPassword: "wrong password value",
|
|
740
|
+
newPassword: "updated password value"
|
|
504
741
|
}
|
|
505
742
|
),
|
|
506
743
|
/Current password is invalid/
|
|
507
744
|
);
|
|
508
745
|
|
|
509
746
|
await authService.changePassword(
|
|
510
|
-
{ cookies: reply.cookies },
|
|
511
747
|
{
|
|
512
|
-
|
|
513
|
-
|
|
748
|
+
cookies: reply.cookies
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
currentPassword: "correct horse battery staple",
|
|
752
|
+
newPassword: "updated password value"
|
|
514
753
|
}
|
|
515
754
|
);
|
|
516
755
|
|
|
517
756
|
await assert.rejects(
|
|
518
757
|
() =>
|
|
519
758
|
authService.login({
|
|
520
|
-
email: "
|
|
521
|
-
password: "
|
|
759
|
+
email: "security@example.com",
|
|
760
|
+
password: "correct horse battery staple"
|
|
522
761
|
}),
|
|
523
762
|
/Invalid email or password/
|
|
524
763
|
);
|
|
525
764
|
const login = await authService.login({
|
|
526
|
-
email: "
|
|
527
|
-
password: "
|
|
765
|
+
email: "security@example.com",
|
|
766
|
+
password: "updated password value"
|
|
528
767
|
});
|
|
529
|
-
assert.equal(login.actor.email, "
|
|
768
|
+
assert.equal(login.actor.email, "security@example.com");
|
|
530
769
|
});
|
|
531
770
|
|
|
532
771
|
test("local auth provider defers profile projector resolution until projection is needed", async () => {
|
|
533
|
-
let
|
|
534
|
-
let projectionCalls = 0;
|
|
772
|
+
let projectorFactoryCalls = 0;
|
|
535
773
|
const { app } = await createStartedApp({
|
|
536
|
-
profileProjectorFactory
|
|
537
|
-
|
|
774
|
+
profileProjectorFactory() {
|
|
775
|
+
projectorFactoryCalls += 1;
|
|
538
776
|
return {
|
|
539
777
|
async syncIdentityProfile(profile) {
|
|
540
|
-
projectionCalls += 1;
|
|
541
778
|
return {
|
|
542
779
|
...profile,
|
|
543
|
-
id: "
|
|
780
|
+
id: "projected-user",
|
|
544
781
|
profileSource: "users"
|
|
545
782
|
};
|
|
546
783
|
}
|
|
547
784
|
};
|
|
548
785
|
}
|
|
549
786
|
});
|
|
550
|
-
|
|
551
|
-
assert.equal(projectorResolved, 0);
|
|
552
|
-
|
|
787
|
+
assert.equal(projectorFactoryCalls, 0);
|
|
553
788
|
const authService = app.make("authService");
|
|
554
|
-
assert.equal(
|
|
555
|
-
assert.equal(projectorResolved, 0);
|
|
556
|
-
|
|
789
|
+
assert.equal(projectorFactoryCalls, 0);
|
|
557
790
|
const registered = await authService.register({
|
|
558
|
-
email: "
|
|
559
|
-
password: "
|
|
560
|
-
displayName: "
|
|
791
|
+
email: "projected@example.com",
|
|
792
|
+
password: "correct horse battery staple",
|
|
793
|
+
displayName: "Projected User"
|
|
561
794
|
});
|
|
562
|
-
|
|
563
|
-
assert.equal(
|
|
564
|
-
assert.equal(projectionCalls, 1);
|
|
565
|
-
assert.equal(registered.actor.appUserId, "app-profile-id");
|
|
795
|
+
assert.equal(projectorFactoryCalls, 1);
|
|
796
|
+
assert.equal(registered.actor.appUserId, "projected-user");
|
|
566
797
|
});
|
|
567
|
-
|
|
568
798
|
test("local auth provider projects app profile when auth.profile.projector is installed", async () => {
|
|
569
799
|
const projectedProfiles = [];
|
|
570
800
|
const { app } = await createStartedApp({
|