@jskit-ai/auth-provider-local-core 0.1.2 → 0.1.3
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
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.3",
|
|
5
5
|
"kind": "runtime",
|
|
6
6
|
"description": "Local auth provider with a file backend default and no database requirement.",
|
|
7
7
|
"dependsOn": [
|
|
@@ -41,13 +41,14 @@ export default Object.freeze({
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"subpath": "./server/lib/index",
|
|
44
|
-
"summary": "Exports local auth service
|
|
44
|
+
"summary": "Exports local auth service, file backend helpers, and password strategy helpers."
|
|
45
45
|
}
|
|
46
46
|
],
|
|
47
47
|
"containerTokens": {
|
|
48
48
|
"server": [
|
|
49
49
|
"authService",
|
|
50
|
-
"auth.local.backend"
|
|
50
|
+
"auth.local.backend",
|
|
51
|
+
"auth.local.passwordStrategy"
|
|
51
52
|
],
|
|
52
53
|
"client": []
|
|
53
54
|
}
|
|
@@ -56,8 +57,8 @@ export default Object.freeze({
|
|
|
56
57
|
"mutations": {
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"runtime": {
|
|
59
|
-
"@jskit-ai/auth-core": "0.1.
|
|
60
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/auth-core": "0.1.102",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.104",
|
|
61
62
|
"nodemailer": "^7.0.10",
|
|
62
63
|
"dotenv": "^16.4.5"
|
|
63
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.3",
|
|
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.102",
|
|
15
|
+
"@jskit-ai/kernel": "0.1.104",
|
|
16
16
|
"nodemailer": "^7.0.10"
|
|
17
17
|
}
|
|
18
18
|
}
|
package/src/server/lib/index.js
CHANGED
|
@@ -41,4 +41,41 @@ async function verifyPassword(password, record) {
|
|
|
41
41
|
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
function normalizePasswordStrategy(strategy = null) {
|
|
45
|
+
if (!strategy) {
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
hashPassword,
|
|
48
|
+
verifyPassword
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof strategy !== "object" || Array.isArray(strategy)) {
|
|
53
|
+
throw new TypeError("Local auth password strategy must be an object.");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const normalized = strategy;
|
|
57
|
+
const strategyHashPassword = typeof normalized.hashPassword === "undefined"
|
|
58
|
+
? hashPassword
|
|
59
|
+
: normalized.hashPassword;
|
|
60
|
+
const strategyVerifyPassword = typeof normalized.verifyPassword === "undefined"
|
|
61
|
+
? verifyPassword
|
|
62
|
+
: normalized.verifyPassword;
|
|
63
|
+
|
|
64
|
+
if (typeof strategyHashPassword !== "function") {
|
|
65
|
+
throw new TypeError("Local auth password strategy hashPassword must be a function.");
|
|
66
|
+
}
|
|
67
|
+
if (typeof strategyVerifyPassword !== "function") {
|
|
68
|
+
throw new TypeError("Local auth password strategy verifyPassword must be a function.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
async hashPassword(password) {
|
|
73
|
+
return strategyHashPassword.call(strategy, password);
|
|
74
|
+
},
|
|
75
|
+
async verifyPassword(password, record) {
|
|
76
|
+
return strategyVerifyPassword.call(strategy, password, record);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { hashPassword, verifyPassword, normalizePasswordStrategy };
|
|
@@ -8,7 +8,7 @@ import { buildSecurityStatusFromAuthMethodsStatus } from "@jskit-ai/auth-core/sh
|
|
|
8
8
|
import { normalizeAuthActor, normalizeAuthResult } from "@jskit-ai/auth-core/server/authActor";
|
|
9
9
|
import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
|
|
10
10
|
import { throwUnsupportedAuthOperation } from "@jskit-ai/auth-core/server/unsupportedOperation";
|
|
11
|
-
import {
|
|
11
|
+
import { normalizePasswordStrategy } from "./passwords.js";
|
|
12
12
|
import { randomToken, sha256Base64url, signToken, verifySignedToken } from "./tokens.js";
|
|
13
13
|
|
|
14
14
|
const ACCESS_TOKEN_COOKIE = "jskit_local_access_token";
|
|
@@ -191,7 +191,7 @@ async function maybeSendRecoveryEmail(config, recoveryUrl, email) {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
function createLocalAuthService({ backend, config, profileProjector = null }) {
|
|
194
|
+
function createLocalAuthService({ backend, config, profileProjector = null, passwordStrategy = null }) {
|
|
195
195
|
if (!backend || typeof backend.withTransaction !== "function") {
|
|
196
196
|
throw new Error("Local auth requires auth.local.backend with withTransaction().");
|
|
197
197
|
}
|
|
@@ -199,6 +199,7 @@ function createLocalAuthService({ backend, config, profileProjector = null }) {
|
|
|
199
199
|
throw new Error("Local auth requires a session secret.");
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
const passwords = normalizePasswordStrategy(passwordStrategy);
|
|
202
203
|
const isProduction = config.nodeEnv === "production";
|
|
203
204
|
const profileProjectionEnabled = typeof profileProjector?.syncIdentityProfile === "function";
|
|
204
205
|
const recoveryDelivery = config.smtpConfigured
|
|
@@ -284,7 +285,7 @@ function createLocalAuthService({ backend, config, profileProjector = null }) {
|
|
|
284
285
|
const email = validateEmailInput(input.email);
|
|
285
286
|
validatePasswordInput(input.password);
|
|
286
287
|
const displayName = normalizeDisplayName(input.displayName, email);
|
|
287
|
-
const password = await hashPassword(input.password);
|
|
288
|
+
const password = await passwords.hashPassword(input.password);
|
|
288
289
|
const result = await backend.withTransaction(async (tx) => {
|
|
289
290
|
const existing = await tx.users.findByEmail(email);
|
|
290
291
|
if (existing) {
|
|
@@ -312,7 +313,7 @@ function createLocalAuthService({ backend, config, profileProjector = null }) {
|
|
|
312
313
|
const password = String(input.password || "");
|
|
313
314
|
return backend.withTransaction(async (tx) => {
|
|
314
315
|
const user = await tx.users.findByEmail(email);
|
|
315
|
-
if (!user || user.disabled || !(await verifyPassword(password, user.password))) {
|
|
316
|
+
if (!user || user.disabled || !(await passwords.verifyPassword(password, user.password))) {
|
|
316
317
|
throw new AppError(401, "Invalid email or password.");
|
|
317
318
|
}
|
|
318
319
|
const session = await createSessionForUser(tx, user);
|
|
@@ -517,7 +518,7 @@ function createLocalAuthService({ backend, config, profileProjector = null }) {
|
|
|
517
518
|
if (!payload?.sub || !payload?.sid) {
|
|
518
519
|
throw new AppError(401, "Authentication required.");
|
|
519
520
|
}
|
|
520
|
-
const password = await hashPassword(input.password);
|
|
521
|
+
const password = await passwords.hashPassword(input.password);
|
|
521
522
|
await backend.withTransaction(async (tx) => {
|
|
522
523
|
const session = await tx.sessions.findById(String(payload.sid));
|
|
523
524
|
if (!session || session.revokedAt || isExpiredIso(session.expiresAt)) {
|
|
@@ -546,13 +547,13 @@ function createLocalAuthService({ backend, config, profileProjector = null }) {
|
|
|
546
547
|
}
|
|
547
548
|
|
|
548
549
|
const currentPassword = String(input.currentPassword || "");
|
|
549
|
-
const password = await hashPassword(input.newPassword);
|
|
550
|
+
const password = await passwords.hashPassword(input.newPassword);
|
|
550
551
|
await backend.withTransaction(async (tx) => {
|
|
551
552
|
const user = await tx.users.findById(authResult.actor.providerUserId);
|
|
552
553
|
if (!user || user.disabled) {
|
|
553
554
|
throw new AppError(401, "Authentication required.");
|
|
554
555
|
}
|
|
555
|
-
if (!(await verifyPassword(currentPassword, user.password))) {
|
|
556
|
+
if (!(await passwords.verifyPassword(currentPassword, user.password))) {
|
|
556
557
|
throw new AppError(401, "Current password is invalid.");
|
|
557
558
|
}
|
|
558
559
|
await tx.users.updatePassword(user.id, password);
|
|
@@ -173,12 +173,24 @@ class AuthLocalServiceProvider {
|
|
|
173
173
|
const config = resolveConfig(scope);
|
|
174
174
|
const backend = scope.make("auth.local.backend");
|
|
175
175
|
const profileProjector = scope.has("auth.profile.projector")
|
|
176
|
-
?
|
|
176
|
+
? {
|
|
177
|
+
async syncIdentityProfile(profile) {
|
|
178
|
+
const projector = scope.make("auth.profile.projector");
|
|
179
|
+
if (!projector || typeof projector.syncIdentityProfile !== "function") {
|
|
180
|
+
throw new Error("auth.profile.projector.syncIdentityProfile() must be a function.");
|
|
181
|
+
}
|
|
182
|
+
return projector.syncIdentityProfile(profile);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
: null;
|
|
186
|
+
const passwordStrategy = scope.has("auth.local.passwordStrategy")
|
|
187
|
+
? scope.make("auth.local.passwordStrategy")
|
|
177
188
|
: null;
|
|
178
189
|
return createLocalAuthService({
|
|
179
190
|
backend,
|
|
180
191
|
config,
|
|
181
|
-
profileProjector
|
|
192
|
+
profileProjector,
|
|
193
|
+
passwordStrategy
|
|
182
194
|
});
|
|
183
195
|
});
|
|
184
196
|
}
|
|
@@ -8,9 +8,13 @@ import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
|
|
|
8
8
|
import { AuthActionsServiceProvider } from "@jskit-ai/auth-core/server/providers/AuthActionsServiceProvider";
|
|
9
9
|
import { AuthLocalServiceProvider } from "../src/server/providers/AuthLocalServiceProvider.js";
|
|
10
10
|
import { AuthProviderServiceProvider } from "../src/server/providers/AuthProviderServiceProvider.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
createLocalAuthService,
|
|
13
|
+
createLocalFileBackend,
|
|
14
|
+
hashPassword,
|
|
15
|
+
normalizePasswordStrategy,
|
|
16
|
+
verifyPassword
|
|
17
|
+
} from "../src/server/lib/index.js";
|
|
14
18
|
|
|
15
19
|
function createAppConfigFixture() {
|
|
16
20
|
return {
|
|
@@ -46,7 +50,7 @@ function createReplyFixture() {
|
|
|
46
50
|
};
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
async function createStartedApp({ profileProjector = null } = {}) {
|
|
53
|
+
async function createStartedApp({ profileProjector = null, profileProjectorFactory = null, passwordStrategy = null } = {}) {
|
|
50
54
|
const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-"));
|
|
51
55
|
const app = createApplication();
|
|
52
56
|
app.instance("appConfig", createAppConfigFixture());
|
|
@@ -69,6 +73,12 @@ async function createStartedApp({ profileProjector = null } = {}) {
|
|
|
69
73
|
if (profileProjector) {
|
|
70
74
|
app.instance("auth.profile.projector", profileProjector);
|
|
71
75
|
}
|
|
76
|
+
if (profileProjectorFactory) {
|
|
77
|
+
app.singleton("auth.profile.projector", profileProjectorFactory);
|
|
78
|
+
}
|
|
79
|
+
if (passwordStrategy) {
|
|
80
|
+
app.instance("auth.local.passwordStrategy", passwordStrategy);
|
|
81
|
+
}
|
|
72
82
|
await app.start({
|
|
73
83
|
providers: [
|
|
74
84
|
ActionRuntimeServiceProvider,
|
|
@@ -134,6 +144,72 @@ test("local auth provider registers, logs in, reads session, and logs out with f
|
|
|
134
144
|
assert.match(usersFile, /^user:v1:/);
|
|
135
145
|
});
|
|
136
146
|
|
|
147
|
+
test("local auth provider resolves a custom password strategy from the container", async () => {
|
|
148
|
+
const passwordStrategy = {
|
|
149
|
+
prefix: "strategy",
|
|
150
|
+
async hashPassword(password) {
|
|
151
|
+
return {
|
|
152
|
+
algorithm: "test-password",
|
|
153
|
+
version: "v1",
|
|
154
|
+
salt: "",
|
|
155
|
+
hash: `${this.prefix}-${password}`
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
async verifyPassword(password, record) {
|
|
159
|
+
return record?.algorithm === "test-password" && record?.hash === `${this.prefix}-${password}`;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const { app } = await createStartedApp({ passwordStrategy });
|
|
163
|
+
const authService = app.make("authService");
|
|
164
|
+
|
|
165
|
+
await authService.register({
|
|
166
|
+
email: "strategy@example.com",
|
|
167
|
+
password: "strategy password value",
|
|
168
|
+
displayName: "Strategy User"
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const loggedIn = await authService.login({
|
|
172
|
+
email: "strategy@example.com",
|
|
173
|
+
password: "strategy password value"
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
assert.equal(loggedIn.actor.email, "strategy@example.com");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("local auth password strategy supports partial overrides and rejects invalid methods", async () => {
|
|
180
|
+
const strategy = normalizePasswordStrategy({
|
|
181
|
+
verifyPassword(password, record) {
|
|
182
|
+
return record === `legacy:${this.realm}:${password}`;
|
|
183
|
+
},
|
|
184
|
+
realm: "users"
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const defaultHashed = await strategy.hashPassword("new password value");
|
|
188
|
+
assert.equal(await verifyPassword("new password value", defaultHashed), true);
|
|
189
|
+
assert.equal(await strategy.verifyPassword("old password value", "legacy:users:old password value"), true);
|
|
190
|
+
|
|
191
|
+
assert.throws(
|
|
192
|
+
() => normalizePasswordStrategy("invalid"),
|
|
193
|
+
/Local auth password strategy must be an object/
|
|
194
|
+
);
|
|
195
|
+
assert.throws(
|
|
196
|
+
() => normalizePasswordStrategy([]),
|
|
197
|
+
/Local auth password strategy must be an object/
|
|
198
|
+
);
|
|
199
|
+
assert.throws(
|
|
200
|
+
() => normalizePasswordStrategy({ hashPassword: "invalid" }),
|
|
201
|
+
/Local auth password strategy hashPassword must be a function/
|
|
202
|
+
);
|
|
203
|
+
assert.throws(
|
|
204
|
+
() => normalizePasswordStrategy({ hashPassword: null }),
|
|
205
|
+
/Local auth password strategy hashPassword must be a function/
|
|
206
|
+
);
|
|
207
|
+
assert.throws(
|
|
208
|
+
() => normalizePasswordStrategy({ verifyPassword: "invalid" }),
|
|
209
|
+
/Local auth password strategy verifyPassword must be a function/
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
137
213
|
test("local auth login verifies password and creates session in one backend transaction", async () => {
|
|
138
214
|
const password = await hashPassword("current password value");
|
|
139
215
|
let transactions = 0;
|
|
@@ -190,6 +266,102 @@ test("local auth login verifies password and creates session in one backend tran
|
|
|
190
266
|
assert.equal(transactions, 1);
|
|
191
267
|
});
|
|
192
268
|
|
|
269
|
+
test("local auth service accepts a custom strategy for legacy stored password records", async () => {
|
|
270
|
+
const usersByEmail = new Map([
|
|
271
|
+
[
|
|
272
|
+
"legacy@example.com",
|
|
273
|
+
{
|
|
274
|
+
id: "usr_legacy",
|
|
275
|
+
email: "legacy@example.com",
|
|
276
|
+
displayName: "Legacy User",
|
|
277
|
+
password: {
|
|
278
|
+
algorithm: "legacy-bcrypt",
|
|
279
|
+
hash: "legacy password value"
|
|
280
|
+
},
|
|
281
|
+
disabled: false
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
]);
|
|
285
|
+
const sessions = [];
|
|
286
|
+
const backend = {
|
|
287
|
+
async withTransaction(callback) {
|
|
288
|
+
const tx = {
|
|
289
|
+
users: {
|
|
290
|
+
async findByEmail(email) {
|
|
291
|
+
return usersByEmail.get(email) || null;
|
|
292
|
+
},
|
|
293
|
+
async create(input) {
|
|
294
|
+
const user = {
|
|
295
|
+
...input,
|
|
296
|
+
disabled: false
|
|
297
|
+
};
|
|
298
|
+
usersByEmail.set(user.email, user);
|
|
299
|
+
return user;
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
sessions: {
|
|
303
|
+
async create(input) {
|
|
304
|
+
const session = {
|
|
305
|
+
...input,
|
|
306
|
+
createdAt: new Date().toISOString(),
|
|
307
|
+
revokedAt: ""
|
|
308
|
+
};
|
|
309
|
+
sessions.push(session);
|
|
310
|
+
return session;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
return callback(tx);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const passwordStrategy = {
|
|
318
|
+
async hashPassword(password) {
|
|
319
|
+
return {
|
|
320
|
+
algorithm: "test-scrypt",
|
|
321
|
+
version: "v1",
|
|
322
|
+
salt: "test",
|
|
323
|
+
hash: `hashed-${password}`
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
async verifyPassword(password, record) {
|
|
327
|
+
if (record?.algorithm === "legacy-bcrypt") {
|
|
328
|
+
return record.hash === password;
|
|
329
|
+
}
|
|
330
|
+
return record?.algorithm === "test-scrypt" && record.hash === `hashed-${password}`;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const authService = createLocalAuthService({
|
|
334
|
+
backend,
|
|
335
|
+
passwordStrategy,
|
|
336
|
+
config: {
|
|
337
|
+
nodeEnv: "test",
|
|
338
|
+
sessionSecret: "test-secret",
|
|
339
|
+
appPublicUrl: "http://localhost:5173",
|
|
340
|
+
smtpConfigured: false,
|
|
341
|
+
recoveryDevOutput: "disabled"
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const legacyLogin = await authService.login({
|
|
346
|
+
email: "legacy@example.com",
|
|
347
|
+
password: "legacy password value"
|
|
348
|
+
});
|
|
349
|
+
assert.equal(legacyLogin.actor.email, "legacy@example.com");
|
|
350
|
+
|
|
351
|
+
await authService.register({
|
|
352
|
+
email: "new-strategy@example.com",
|
|
353
|
+
password: "new password value",
|
|
354
|
+
displayName: "New Strategy"
|
|
355
|
+
});
|
|
356
|
+
assert.deepEqual(usersByEmail.get("new-strategy@example.com").password, {
|
|
357
|
+
algorithm: "test-scrypt",
|
|
358
|
+
version: "v1",
|
|
359
|
+
salt: "test",
|
|
360
|
+
hash: "hashed-new password value"
|
|
361
|
+
});
|
|
362
|
+
assert.equal(sessions.length, 2);
|
|
363
|
+
});
|
|
364
|
+
|
|
193
365
|
test("local auth provider completes recovery through a recovery-scoped session", async () => {
|
|
194
366
|
const { app } = await createStartedApp();
|
|
195
367
|
const authService = app.make("authService");
|
|
@@ -349,6 +521,42 @@ test("local auth provider changes password through the account-security contract
|
|
|
349
521
|
assert.equal(login.actor.email, "lin@example.com");
|
|
350
522
|
});
|
|
351
523
|
|
|
524
|
+
test("local auth provider defers profile projector resolution until projection is needed", async () => {
|
|
525
|
+
let projectorResolved = 0;
|
|
526
|
+
let projectionCalls = 0;
|
|
527
|
+
const { app } = await createStartedApp({
|
|
528
|
+
profileProjectorFactory: () => {
|
|
529
|
+
projectorResolved += 1;
|
|
530
|
+
return {
|
|
531
|
+
async syncIdentityProfile(profile) {
|
|
532
|
+
projectionCalls += 1;
|
|
533
|
+
return {
|
|
534
|
+
...profile,
|
|
535
|
+
id: "app-profile-id",
|
|
536
|
+
profileSource: "users"
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
assert.equal(projectorResolved, 0);
|
|
544
|
+
|
|
545
|
+
const authService = app.make("authService");
|
|
546
|
+
assert.equal(authService.getCapabilities().features.appProfileProjection, true);
|
|
547
|
+
assert.equal(projectorResolved, 0);
|
|
548
|
+
|
|
549
|
+
const registered = await authService.register({
|
|
550
|
+
email: "projector@example.com",
|
|
551
|
+
password: "projector password value",
|
|
552
|
+
displayName: "Projector User"
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
assert.equal(projectorResolved, 1);
|
|
556
|
+
assert.equal(projectionCalls, 1);
|
|
557
|
+
assert.equal(registered.actor.appUserId, "app-profile-id");
|
|
558
|
+
});
|
|
559
|
+
|
|
352
560
|
test("local auth provider projects app profile when auth.profile.projector is installed", async () => {
|
|
353
561
|
const projectedProfiles = [];
|
|
354
562
|
const { app } = await createStartedApp({
|