@jskit-ai/auth-provider-local-core 0.1.1
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 +102 -0
- package/package.json +18 -0
- package/src/server/lib/fileBackend.js +496 -0
- package/src/server/lib/index.js +2 -0
- package/src/server/lib/passwords.js +44 -0
- package/src/server/lib/service.js +689 -0
- package/src/server/lib/tokens.js +57 -0
- package/src/server/providers/AuthLocalServiceProvider.js +194 -0
- package/src/server/providers/AuthProviderServiceProvider.js +9 -0
- package/test/providerRuntime.test.js +505 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { createApplication } from "@jskit-ai/kernel/_testable";
|
|
7
|
+
import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
|
|
8
|
+
import { AuthActionsServiceProvider } from "@jskit-ai/auth-core/server/providers/AuthActionsServiceProvider";
|
|
9
|
+
import { AuthLocalServiceProvider } from "../src/server/providers/AuthLocalServiceProvider.js";
|
|
10
|
+
import { AuthProviderServiceProvider } from "../src/server/providers/AuthProviderServiceProvider.js";
|
|
11
|
+
import { createLocalFileBackend } from "../src/server/lib/fileBackend.js";
|
|
12
|
+
import { createLocalAuthService } from "../src/server/lib/service.js";
|
|
13
|
+
import { hashPassword } from "../src/server/lib/passwords.js";
|
|
14
|
+
|
|
15
|
+
function createAppConfigFixture() {
|
|
16
|
+
return {
|
|
17
|
+
surfaceModeAll: "all",
|
|
18
|
+
surfaceDefaultId: "home",
|
|
19
|
+
surfaceDefinitions: {
|
|
20
|
+
home: { id: "home", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false },
|
|
21
|
+
console: {
|
|
22
|
+
id: "console",
|
|
23
|
+
pagesRoot: "console",
|
|
24
|
+
enabled: true,
|
|
25
|
+
requiresAuth: true,
|
|
26
|
+
requiresWorkspace: false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createReplyFixture() {
|
|
33
|
+
const cookies = {};
|
|
34
|
+
const cookieOptions = {};
|
|
35
|
+
return {
|
|
36
|
+
cookies,
|
|
37
|
+
cookieOptions,
|
|
38
|
+
setCookie(name, value, options) {
|
|
39
|
+
cookies[name] = value;
|
|
40
|
+
cookieOptions[name] = options;
|
|
41
|
+
},
|
|
42
|
+
clearCookie(name) {
|
|
43
|
+
delete cookies[name];
|
|
44
|
+
delete cookieOptions[name];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function createStartedApp({ profileProjector = null } = {}) {
|
|
50
|
+
const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-"));
|
|
51
|
+
const app = createApplication();
|
|
52
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
53
|
+
app.instance("jskit.env", {
|
|
54
|
+
AUTH_PROVIDER: "local",
|
|
55
|
+
AUTH_LOCAL_STORE_DIR: storeDir,
|
|
56
|
+
AUTH_LOCAL_RECOVERY_DEV_OUTPUT: "response",
|
|
57
|
+
APP_PUBLIC_URL: "http://localhost:5173",
|
|
58
|
+
NODE_ENV: "test"
|
|
59
|
+
});
|
|
60
|
+
app.instance("jskit.logger", {
|
|
61
|
+
info() {},
|
|
62
|
+
warn() {},
|
|
63
|
+
error() {},
|
|
64
|
+
debug() {}
|
|
65
|
+
});
|
|
66
|
+
app.instance("domainEvents", {
|
|
67
|
+
async publish() {}
|
|
68
|
+
});
|
|
69
|
+
if (profileProjector) {
|
|
70
|
+
app.instance("auth.profile.projector", profileProjector);
|
|
71
|
+
}
|
|
72
|
+
await app.start({
|
|
73
|
+
providers: [
|
|
74
|
+
ActionRuntimeServiceProvider,
|
|
75
|
+
AuthLocalServiceProvider,
|
|
76
|
+
AuthProviderServiceProvider,
|
|
77
|
+
AuthActionsServiceProvider
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
app,
|
|
82
|
+
storeDir
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test("local auth provider registers, logs in, reads session, and logs out with file backend", async () => {
|
|
87
|
+
const { app, storeDir } = await createStartedApp();
|
|
88
|
+
const authService = app.make("authService");
|
|
89
|
+
const capabilities = authService.getCapabilities();
|
|
90
|
+
assert.equal(capabilities.provider.id, "local");
|
|
91
|
+
assert.equal(capabilities.features.password.login, true);
|
|
92
|
+
assert.equal(capabilities.features.oauthLogin.enabled, false);
|
|
93
|
+
|
|
94
|
+
const registered = await authService.register({
|
|
95
|
+
email: "Ada@example.com",
|
|
96
|
+
password: "correct horse battery staple",
|
|
97
|
+
displayName: "Ada"
|
|
98
|
+
});
|
|
99
|
+
assert.equal(registered.profile.email, "ada@example.com");
|
|
100
|
+
assert.equal(registered.actor.provider, "local");
|
|
101
|
+
assert.equal(registered.requiresEmailConfirmation, false);
|
|
102
|
+
|
|
103
|
+
const reply = createReplyFixture();
|
|
104
|
+
authService.writeSessionCookies(reply, registered.session);
|
|
105
|
+
assert.equal(Boolean(reply.cookies.jskit_local_access_token), true);
|
|
106
|
+
assert.equal(Boolean(reply.cookies.jskit_local_refresh_token), true);
|
|
107
|
+
|
|
108
|
+
const session = await authService.authenticateRequest({ cookies: reply.cookies });
|
|
109
|
+
assert.equal(session.authenticated, true);
|
|
110
|
+
assert.equal(session.actor.email, "ada@example.com");
|
|
111
|
+
|
|
112
|
+
const actionExecutor = app.make("actionExecutor");
|
|
113
|
+
const definitions = actionExecutor.listDefinitions();
|
|
114
|
+
assert.equal(definitions.some((definition) => definition.id === "auth.login.password"), true);
|
|
115
|
+
const actionSession = await actionExecutor.execute({
|
|
116
|
+
actionId: "auth.session.read",
|
|
117
|
+
input: {},
|
|
118
|
+
context: {
|
|
119
|
+
channel: "internal",
|
|
120
|
+
surface: "home",
|
|
121
|
+
requestMeta: {
|
|
122
|
+
request: { cookies: reply.cookies }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
assert.equal(actionSession.authenticated, true);
|
|
127
|
+
|
|
128
|
+
await authService.logout({ cookies: reply.cookies });
|
|
129
|
+
const loggedOut = await authService.authenticateRequest({ cookies: reply.cookies });
|
|
130
|
+
assert.equal(loggedOut.authenticated, false);
|
|
131
|
+
assert.equal(loggedOut.clearSession, true);
|
|
132
|
+
|
|
133
|
+
const usersFile = await fs.readFile(path.join(storeDir, "users.passwd"), "utf8");
|
|
134
|
+
assert.match(usersFile, /^user:v1:/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("local auth login verifies password and creates session in one backend transaction", async () => {
|
|
138
|
+
const password = await hashPassword("current password value");
|
|
139
|
+
let transactions = 0;
|
|
140
|
+
let sessionCreated = false;
|
|
141
|
+
const backend = {
|
|
142
|
+
async withTransaction(callback) {
|
|
143
|
+
transactions += 1;
|
|
144
|
+
const tx = {
|
|
145
|
+
users: {
|
|
146
|
+
async findByEmail(email) {
|
|
147
|
+
assert.equal(email, "race@example.com");
|
|
148
|
+
return {
|
|
149
|
+
id: "usr_race",
|
|
150
|
+
email,
|
|
151
|
+
displayName: "Race User",
|
|
152
|
+
password,
|
|
153
|
+
disabled: false
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
sessions: {
|
|
158
|
+
async create(input) {
|
|
159
|
+
sessionCreated = true;
|
|
160
|
+
return {
|
|
161
|
+
...input,
|
|
162
|
+
createdAt: new Date().toISOString(),
|
|
163
|
+
revokedAt: ""
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
return callback(tx);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const authService = createLocalAuthService({
|
|
172
|
+
backend,
|
|
173
|
+
config: {
|
|
174
|
+
nodeEnv: "test",
|
|
175
|
+
sessionSecret: "test-secret",
|
|
176
|
+
appPublicUrl: "http://localhost:5173",
|
|
177
|
+
smtpConfigured: false,
|
|
178
|
+
recoveryDevOutput: "disabled"
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await authService.login({
|
|
183
|
+
email: "race@example.com",
|
|
184
|
+
password: "current password value"
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
assert.equal(result.actor.email, "race@example.com");
|
|
188
|
+
assert.equal(Boolean(result.session?.access_token), true);
|
|
189
|
+
assert.equal(sessionCreated, true);
|
|
190
|
+
assert.equal(transactions, 1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("local auth provider completes recovery through a recovery-scoped session", async () => {
|
|
194
|
+
const { app } = await createStartedApp();
|
|
195
|
+
const authService = app.make("authService");
|
|
196
|
+
const registered = await authService.register({
|
|
197
|
+
email: "grace@example.com",
|
|
198
|
+
password: "old password value",
|
|
199
|
+
displayName: "Grace"
|
|
200
|
+
});
|
|
201
|
+
const normalReply = createReplyFixture();
|
|
202
|
+
authService.writeSessionCookies(normalReply, registered.session);
|
|
203
|
+
|
|
204
|
+
await assert.rejects(
|
|
205
|
+
() =>
|
|
206
|
+
authService.resetPassword(
|
|
207
|
+
{
|
|
208
|
+
cookies: normalReply.cookies
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
password: "bypassed password value"
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
/Authentication required/
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const resetRequest = await authService.requestPasswordReset({
|
|
218
|
+
email: "grace@example.com"
|
|
219
|
+
});
|
|
220
|
+
assert.equal(resetRequest.ok, true);
|
|
221
|
+
assert.match(resetRequest.recoveryUrl, /\/auth\/reset-password\?token=/);
|
|
222
|
+
const recoveryToken = new URL(resetRequest.recoveryUrl).searchParams.get("token");
|
|
223
|
+
|
|
224
|
+
const recovery = await authService.completePasswordRecovery({
|
|
225
|
+
code: recoveryToken,
|
|
226
|
+
type: "recovery"
|
|
227
|
+
});
|
|
228
|
+
const reply = createReplyFixture();
|
|
229
|
+
authService.writeSessionCookies(reply, recovery.session);
|
|
230
|
+
assert.equal(Boolean(reply.cookies.jskit_local_recovery_token), true);
|
|
231
|
+
assert.equal(reply.cookieOptions.jskit_local_recovery_token.maxAge, 15 * 60);
|
|
232
|
+
assert.equal(reply.cookieOptions.jskit_local_refresh_token.maxAge, 15 * 60);
|
|
233
|
+
|
|
234
|
+
const generalSession = await authService.authenticateRequest({ cookies: reply.cookies });
|
|
235
|
+
assert.equal(generalSession.authenticated, false);
|
|
236
|
+
assert.equal(generalSession.clearSession, false);
|
|
237
|
+
|
|
238
|
+
await authService.resetPassword(
|
|
239
|
+
{
|
|
240
|
+
cookies: reply.cookies
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
password: "new password value"
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
await assert.rejects(
|
|
248
|
+
() =>
|
|
249
|
+
authService.login({
|
|
250
|
+
email: "grace@example.com",
|
|
251
|
+
password: "old password value"
|
|
252
|
+
}),
|
|
253
|
+
/Invalid email or password/
|
|
254
|
+
);
|
|
255
|
+
const login = await authService.login({
|
|
256
|
+
email: "grace@example.com",
|
|
257
|
+
password: "new password value"
|
|
258
|
+
});
|
|
259
|
+
assert.equal(login.actor.email, "grace@example.com");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("local auth reset invalidates other outstanding recovery tokens for the user", async () => {
|
|
263
|
+
const { app } = await createStartedApp();
|
|
264
|
+
const authService = app.make("authService");
|
|
265
|
+
await authService.register({
|
|
266
|
+
email: "multi-reset@example.com",
|
|
267
|
+
password: "old password value",
|
|
268
|
+
displayName: "Multi Reset"
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const firstResetRequest = await authService.requestPasswordReset({
|
|
272
|
+
email: "multi-reset@example.com"
|
|
273
|
+
});
|
|
274
|
+
const secondResetRequest = await authService.requestPasswordReset({
|
|
275
|
+
email: "multi-reset@example.com"
|
|
276
|
+
});
|
|
277
|
+
const firstToken = new URL(firstResetRequest.recoveryUrl).searchParams.get("token");
|
|
278
|
+
const secondToken = new URL(secondResetRequest.recoveryUrl).searchParams.get("token");
|
|
279
|
+
|
|
280
|
+
const recovery = await authService.completePasswordRecovery({
|
|
281
|
+
code: firstToken,
|
|
282
|
+
type: "recovery"
|
|
283
|
+
});
|
|
284
|
+
const reply = createReplyFixture();
|
|
285
|
+
authService.writeSessionCookies(reply, recovery.session);
|
|
286
|
+
|
|
287
|
+
await authService.resetPassword(
|
|
288
|
+
{
|
|
289
|
+
cookies: reply.cookies
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
password: "new password value"
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
await assert.rejects(
|
|
297
|
+
() =>
|
|
298
|
+
authService.completePasswordRecovery({
|
|
299
|
+
code: secondToken,
|
|
300
|
+
type: "recovery"
|
|
301
|
+
}),
|
|
302
|
+
/Recovery token is invalid or expired/
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("local auth provider changes password through the account-security contract", async () => {
|
|
307
|
+
const { app } = await createStartedApp();
|
|
308
|
+
const authService = app.make("authService");
|
|
309
|
+
const registered = await authService.register({
|
|
310
|
+
email: "lin@example.com",
|
|
311
|
+
password: "old password value",
|
|
312
|
+
displayName: "Lin"
|
|
313
|
+
});
|
|
314
|
+
const reply = createReplyFixture();
|
|
315
|
+
authService.writeSessionCookies(reply, registered.session);
|
|
316
|
+
|
|
317
|
+
await assert.rejects(
|
|
318
|
+
() =>
|
|
319
|
+
authService.changePassword(
|
|
320
|
+
{ cookies: reply.cookies },
|
|
321
|
+
{
|
|
322
|
+
currentPassword: "wrong password",
|
|
323
|
+
newPassword: "new password value"
|
|
324
|
+
}
|
|
325
|
+
),
|
|
326
|
+
/Current password is invalid/
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
await authService.changePassword(
|
|
330
|
+
{ cookies: reply.cookies },
|
|
331
|
+
{
|
|
332
|
+
currentPassword: "old password value",
|
|
333
|
+
newPassword: "new password value"
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
await assert.rejects(
|
|
338
|
+
() =>
|
|
339
|
+
authService.login({
|
|
340
|
+
email: "lin@example.com",
|
|
341
|
+
password: "old password value"
|
|
342
|
+
}),
|
|
343
|
+
/Invalid email or password/
|
|
344
|
+
);
|
|
345
|
+
const login = await authService.login({
|
|
346
|
+
email: "lin@example.com",
|
|
347
|
+
password: "new password value"
|
|
348
|
+
});
|
|
349
|
+
assert.equal(login.actor.email, "lin@example.com");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("local auth provider projects app profile when auth.profile.projector is installed", async () => {
|
|
353
|
+
const projectedProfiles = [];
|
|
354
|
+
const { app } = await createStartedApp({
|
|
355
|
+
profileProjector: {
|
|
356
|
+
async syncIdentityProfile(profile) {
|
|
357
|
+
projectedProfiles.push(profile);
|
|
358
|
+
return {
|
|
359
|
+
...profile,
|
|
360
|
+
id: "app-user-1",
|
|
361
|
+
profileSource: "users"
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
const authService = app.make("authService");
|
|
367
|
+
assert.equal(authService.getCapabilities().features.appProfileProjection, true);
|
|
368
|
+
|
|
369
|
+
const registered = await authService.register({
|
|
370
|
+
email: "projected@example.com",
|
|
371
|
+
password: "projected password",
|
|
372
|
+
displayName: "Projected"
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
assert.equal(registered.profile.id, "app-user-1");
|
|
376
|
+
assert.equal(registered.actor.providerUserId, projectedProfiles[0].authProviderUserSid);
|
|
377
|
+
assert.equal(registered.actor.appUserId, "app-user-1");
|
|
378
|
+
assert.equal(registered.actor.profileSource, "users");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("local auth provider rejects AUTH_PROVIDER mismatches", async () => {
|
|
382
|
+
const app = createApplication();
|
|
383
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
384
|
+
app.instance("jskit.env", {
|
|
385
|
+
AUTH_PROVIDER: "supabase",
|
|
386
|
+
NODE_ENV: "test"
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await assert.rejects(
|
|
390
|
+
() =>
|
|
391
|
+
app.start({
|
|
392
|
+
providers: [ActionRuntimeServiceProvider, AuthLocalServiceProvider]
|
|
393
|
+
}),
|
|
394
|
+
(error) => /AUTH_PROVIDER is "supabase"/.test(String(error.details?.cause?.message || error.message || ""))
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("local auth provider requires an explicit public URL for SMTP recovery", async () => {
|
|
399
|
+
const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-"));
|
|
400
|
+
const app = createApplication();
|
|
401
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
402
|
+
app.instance("jskit.env", {
|
|
403
|
+
AUTH_PROVIDER: "local",
|
|
404
|
+
AUTH_LOCAL_STORE_DIR: storeDir,
|
|
405
|
+
AUTH_LOCAL_SMTP_HOST: "smtp.example.com",
|
|
406
|
+
AUTH_LOCAL_SMTP_FROM: "support@example.com",
|
|
407
|
+
NODE_ENV: "test"
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await assert.rejects(
|
|
411
|
+
() =>
|
|
412
|
+
app.start({
|
|
413
|
+
providers: [ActionRuntimeServiceProvider, AuthLocalServiceProvider]
|
|
414
|
+
}),
|
|
415
|
+
(error) =>
|
|
416
|
+
/APP_PUBLIC_URL is required when local auth SMTP recovery is configured/.test(
|
|
417
|
+
String(error.details?.cause?.message || error.message || "")
|
|
418
|
+
)
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("local file backend recovers stale transaction locks", async () => {
|
|
423
|
+
const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-"));
|
|
424
|
+
await fs.mkdir(storeDir, { recursive: true });
|
|
425
|
+
const lockPath = path.join(storeDir, "store.lock");
|
|
426
|
+
await fs.writeFile(lockPath, "0:stale\n", "utf8");
|
|
427
|
+
const staleTime = new Date(Date.now() - 60_000);
|
|
428
|
+
await fs.utimes(lockPath, staleTime, staleTime);
|
|
429
|
+
|
|
430
|
+
const backend = createLocalFileBackend({ storeDir });
|
|
431
|
+
await backend.withTransaction(async (tx) => {
|
|
432
|
+
await tx.users.create({
|
|
433
|
+
id: "usr_stale_lock",
|
|
434
|
+
email: "stale-lock@example.com",
|
|
435
|
+
displayName: "Stale Lock",
|
|
436
|
+
password: {
|
|
437
|
+
algorithm: "test",
|
|
438
|
+
version: "1",
|
|
439
|
+
salt: "salt",
|
|
440
|
+
hash: "hash"
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const user = await backend.withTransaction((tx) => tx.users.findByEmail("stale-lock@example.com"));
|
|
446
|
+
assert.equal(user.id, "usr_stale_lock");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("local file backend replays a pending multi-file transaction journal before reads", async () => {
|
|
450
|
+
const sourceDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-source-"));
|
|
451
|
+
const sourceBackend = createLocalFileBackend({ storeDir: sourceDir });
|
|
452
|
+
await sourceBackend.withTransaction(async (tx) => {
|
|
453
|
+
await tx.users.create({
|
|
454
|
+
id: "usr_journal",
|
|
455
|
+
email: "journal@example.com",
|
|
456
|
+
displayName: "Journal User",
|
|
457
|
+
password: {
|
|
458
|
+
algorithm: "test",
|
|
459
|
+
version: "1",
|
|
460
|
+
salt: "salt",
|
|
461
|
+
hash: "hash"
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
await tx.sessions.create({
|
|
465
|
+
id: "ses_journal",
|
|
466
|
+
userId: "usr_journal",
|
|
467
|
+
tokenHash: "session_hash",
|
|
468
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString()
|
|
469
|
+
});
|
|
470
|
+
await tx.recovery.create({
|
|
471
|
+
id: "rec_journal",
|
|
472
|
+
userId: "usr_journal",
|
|
473
|
+
tokenHash: "recovery_hash",
|
|
474
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString()
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-journal-"));
|
|
479
|
+
const files = await Promise.all(
|
|
480
|
+
["users.passwd", "sessions.passwd", "recovery.passwd"].map(async (name) => ({
|
|
481
|
+
name,
|
|
482
|
+
content: Buffer.from(await fs.readFile(path.join(sourceDir, name), "utf8"), "utf8").toString("base64url")
|
|
483
|
+
}))
|
|
484
|
+
);
|
|
485
|
+
await fs.writeFile(
|
|
486
|
+
path.join(storeDir, "transaction.journal"),
|
|
487
|
+
`${JSON.stringify({
|
|
488
|
+
version: 1,
|
|
489
|
+
files
|
|
490
|
+
})}\n`,
|
|
491
|
+
"utf8"
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const backend = createLocalFileBackend({ storeDir });
|
|
495
|
+
const recovered = await backend.withTransaction(async (tx) => ({
|
|
496
|
+
user: await tx.users.findByEmail("journal@example.com"),
|
|
497
|
+
session: await tx.sessions.findByTokenHash("session_hash"),
|
|
498
|
+
recovery: await tx.recovery.findByTokenHash("recovery_hash")
|
|
499
|
+
}));
|
|
500
|
+
|
|
501
|
+
assert.equal(recovered.user.id, "usr_journal");
|
|
502
|
+
assert.equal(recovered.session.id, "ses_journal");
|
|
503
|
+
assert.equal(recovered.recovery.id, "rec_journal");
|
|
504
|
+
await assert.rejects(() => fs.stat(path.join(storeDir, "transaction.journal")), { code: "ENOENT" });
|
|
505
|
+
});
|