@webstir-io/webstir-backend 0.1.15 → 0.1.16
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/README.md +106 -79
- package/dist/add.d.ts +59 -0
- package/dist/add.js +626 -0
- package/dist/build/artifacts.d.ts +115 -1
- package/dist/build/artifacts.js +4 -4
- package/dist/build/entries.js +1 -1
- package/dist/build/pipeline.d.ts +33 -1
- package/dist/build/pipeline.js +307 -65
- package/dist/cache/diff.js +9 -8
- package/dist/cache/reporters.js +1 -1
- package/dist/deploy-cli.d.ts +2 -0
- package/dist/deploy-cli.js +86 -0
- package/dist/diagnostics/summary.js +2 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/manifest/pipeline.js +103 -32
- package/dist/provider.js +35 -17
- package/dist/runtime/bun.d.ts +51 -0
- package/dist/runtime/bun.js +499 -0
- package/dist/runtime/core.d.ts +141 -0
- package/dist/runtime/core.js +316 -0
- package/dist/runtime/deploy-backend.d.ts +20 -0
- package/dist/runtime/deploy-backend.js +175 -0
- package/dist/runtime/deploy-shared.d.ts +43 -0
- package/dist/runtime/deploy-shared.js +75 -0
- package/dist/runtime/deploy-static.d.ts +2 -0
- package/dist/runtime/deploy-static.js +161 -0
- package/dist/runtime/deploy.d.ts +3 -0
- package/dist/runtime/deploy.js +91 -0
- package/dist/runtime/forms.d.ts +73 -0
- package/dist/runtime/forms.js +236 -0
- package/dist/runtime/request-hooks.d.ts +47 -0
- package/dist/runtime/request-hooks.js +102 -0
- package/dist/runtime/session-metadata.d.ts +13 -0
- package/dist/runtime/session-metadata.js +98 -0
- package/dist/runtime/session-runtime.d.ts +28 -0
- package/dist/runtime/session-runtime.js +180 -0
- package/dist/runtime/session.d.ts +83 -0
- package/dist/runtime/session.js +396 -0
- package/dist/runtime/views.d.ts +74 -0
- package/dist/runtime/views.js +221 -0
- package/dist/scaffold/assets.js +25 -21
- package/dist/testing/context.js +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +100 -56
- package/dist/utils/bun.d.ts +2 -0
- package/dist/utils/bun.js +13 -0
- package/dist/watch.d.ts +13 -1
- package/dist/watch.js +345 -97
- package/dist/workspace.d.ts +8 -0
- package/dist/workspace.js +44 -3
- package/package.json +49 -14
- package/scripts/publish.sh +2 -92
- package/scripts/smoke.mjs +282 -107
- package/scripts/update-contract.sh +12 -10
- package/src/add.ts +964 -0
- package/src/build/artifacts.ts +49 -46
- package/src/build/entries.ts +12 -12
- package/src/build/pipeline.ts +779 -403
- package/src/cache/diff.ts +111 -105
- package/src/cache/reporters.ts +26 -26
- package/src/deploy-cli.ts +111 -0
- package/src/diagnostics/summary.ts +28 -22
- package/src/index.ts +11 -0
- package/src/manifest/pipeline.ts +328 -215
- package/src/provider.ts +115 -98
- package/src/runtime/bun.ts +793 -0
- package/src/runtime/core.ts +598 -0
- package/src/runtime/deploy-backend.ts +239 -0
- package/src/runtime/deploy-shared.ts +136 -0
- package/src/runtime/deploy-static.ts +191 -0
- package/src/runtime/deploy.ts +143 -0
- package/src/runtime/forms.ts +364 -0
- package/src/runtime/request-hooks.ts +165 -0
- package/src/runtime/session-metadata.ts +135 -0
- package/src/runtime/session-runtime.ts +267 -0
- package/src/runtime/session.ts +642 -0
- package/src/runtime/views.ts +385 -0
- package/src/scaffold/assets.ts +77 -73
- package/src/testing/context.js +8 -9
- package/src/testing/context.ts +9 -9
- package/src/testing/index.d.ts +14 -3
- package/src/testing/index.js +254 -175
- package/src/testing/index.ts +298 -195
- package/src/testing/types.d.ts +18 -19
- package/src/testing/types.ts +18 -18
- package/src/utils/bun.ts +26 -0
- package/src/watch.ts +503 -99
- package/src/workspace.ts +59 -3
- package/templates/backend/.env.example +15 -0
- package/templates/backend/auth/adapter.ts +335 -36
- package/templates/backend/db/connection.ts +190 -65
- package/templates/backend/db/migrate.ts +149 -43
- package/templates/backend/db/types.d.ts +1 -1
- package/templates/backend/env.ts +132 -20
- package/templates/backend/functions/hello/index.ts +1 -2
- package/templates/backend/index.ts +15 -508
- package/templates/backend/jobs/nightly/index.ts +1 -1
- package/templates/backend/jobs/runtime.ts +24 -11
- package/templates/backend/jobs/scheduler.ts +208 -46
- package/templates/backend/module.ts +227 -13
- package/templates/backend/observability/logger.ts +2 -12
- package/templates/backend/observability/metrics.ts +8 -5
- package/templates/backend/session/sqlite.ts +152 -0
- package/templates/backend/session/store.ts +45 -0
- package/templates/backend/tsconfig.json +1 -1
- package/tests/add.test.js +327 -0
- package/tests/authAdapter.test.js +315 -0
- package/tests/bundlerParity.test.js +217 -0
- package/tests/cacheReporter.test.js +10 -10
- package/tests/dbConnection.test.js +209 -0
- package/tests/deploy.test.js +357 -0
- package/tests/envLoader.test.js +271 -17
- package/tests/integration.test.js +2432 -3
- package/tests/jobsScheduler.test.js +253 -0
- package/tests/manifest.test.js +287 -12
- package/tests/migrationRunner.test.js +249 -0
- package/tests/sessionScaffoldStore.test.js +752 -0
- package/tests/sessionStore.test.js +490 -0
- package/tests/testing.test.js +252 -0
- package/tests/watch.test.js +192 -32
- package/tsconfig.json +3 -10
- package/templates/backend/server/fastify.ts +0 -288
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createInMemorySessionStore,
|
|
6
|
+
prepareSessionState,
|
|
7
|
+
resetInMemorySessionStore,
|
|
8
|
+
} from '../dist/runtime/session.js';
|
|
9
|
+
import { prepareFormState, processFormSubmission } from '../dist/runtime/forms.js';
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
secret: 'test-session-secret',
|
|
13
|
+
cookieName: 'webstir_session',
|
|
14
|
+
secure: false,
|
|
15
|
+
maxAgeSeconds: 60,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const loginRoute = {
|
|
19
|
+
form: {
|
|
20
|
+
session: { write: true },
|
|
21
|
+
flash: {
|
|
22
|
+
publish: [{ key: 'signed-in', level: 'success', when: 'success' }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const accountRoute = {
|
|
28
|
+
session: { mode: 'optional' },
|
|
29
|
+
flash: { consume: ['signed-in'] },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
test('prepareSessionState honors an injected in-memory store boundary', () => {
|
|
33
|
+
const store = createInMemorySessionStore();
|
|
34
|
+
resetInMemorySessionStore();
|
|
35
|
+
|
|
36
|
+
const created = prepareSessionState({
|
|
37
|
+
cookies: '',
|
|
38
|
+
route: loginRoute,
|
|
39
|
+
config,
|
|
40
|
+
store,
|
|
41
|
+
});
|
|
42
|
+
const createdCommit = created.commit({
|
|
43
|
+
session: {
|
|
44
|
+
userId: 'ada@example.com',
|
|
45
|
+
data: { email: 'ada@example.com' },
|
|
46
|
+
},
|
|
47
|
+
route: loginRoute,
|
|
48
|
+
result: {
|
|
49
|
+
status: 303,
|
|
50
|
+
redirect: { location: '/session/account' },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const cookieHeader = extractCookieHeader(createdCommit.setCookie);
|
|
54
|
+
|
|
55
|
+
const globalRead = prepareSessionState({
|
|
56
|
+
cookies: cookieHeader,
|
|
57
|
+
route: accountRoute,
|
|
58
|
+
config,
|
|
59
|
+
});
|
|
60
|
+
assert.equal(globalRead.session, null);
|
|
61
|
+
assert.deepEqual(globalRead.flash, []);
|
|
62
|
+
|
|
63
|
+
const scopedRead = prepareSessionState({
|
|
64
|
+
cookies: cookieHeader,
|
|
65
|
+
route: accountRoute,
|
|
66
|
+
config,
|
|
67
|
+
store,
|
|
68
|
+
});
|
|
69
|
+
assert.equal(scopedRead.session?.userId, 'ada@example.com');
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
scopedRead.flash.map((message) => ({ key: message.key, level: message.level })),
|
|
72
|
+
[{ key: 'signed-in', level: 'success' }],
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('prepareSessionState clears expired records and stale or tampered cookies on commit', () => {
|
|
77
|
+
const store = createInMemorySessionStore();
|
|
78
|
+
const created = prepareSessionState({
|
|
79
|
+
cookies: '',
|
|
80
|
+
route: loginRoute,
|
|
81
|
+
config,
|
|
82
|
+
store,
|
|
83
|
+
now: () => new Date('2026-01-01T00:00:00.000Z'),
|
|
84
|
+
});
|
|
85
|
+
const createdCommit = created.commit({
|
|
86
|
+
session: {
|
|
87
|
+
userId: 'ada@example.com',
|
|
88
|
+
},
|
|
89
|
+
route: loginRoute,
|
|
90
|
+
result: {
|
|
91
|
+
status: 303,
|
|
92
|
+
redirect: { location: '/session/account' },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const cookieHeader = extractCookieHeader(createdCommit.setCookie);
|
|
96
|
+
const sessionId = extractSessionId(cookieHeader, config.cookieName);
|
|
97
|
+
const stored = store.get(sessionId);
|
|
98
|
+
|
|
99
|
+
assert.ok(stored, 'expected stored session record');
|
|
100
|
+
store.set({
|
|
101
|
+
...stored,
|
|
102
|
+
expiresAt: '2025-12-31T23:59:59.000Z',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const expired = prepareSessionState({
|
|
106
|
+
cookies: cookieHeader,
|
|
107
|
+
route: accountRoute,
|
|
108
|
+
config,
|
|
109
|
+
store,
|
|
110
|
+
now: () => new Date('2026-01-01T00:00:00.000Z'),
|
|
111
|
+
});
|
|
112
|
+
assert.equal(expired.session, null);
|
|
113
|
+
assert.equal(store.get(sessionId), undefined);
|
|
114
|
+
const expiredCommit = expired.commit({
|
|
115
|
+
session: expired.session,
|
|
116
|
+
route: accountRoute,
|
|
117
|
+
result: { status: 200 },
|
|
118
|
+
});
|
|
119
|
+
assert.match(String(expiredCommit.setCookie), /^webstir_session=;.*Max-Age=0/);
|
|
120
|
+
|
|
121
|
+
const tampered = prepareSessionState({
|
|
122
|
+
cookies: cookieHeader.replace(/\.[^.;]+/, '.invalid-signature'),
|
|
123
|
+
route: accountRoute,
|
|
124
|
+
config,
|
|
125
|
+
store,
|
|
126
|
+
});
|
|
127
|
+
assert.equal(tampered.session, null);
|
|
128
|
+
const tamperedCommit = tampered.commit({
|
|
129
|
+
session: tampered.session,
|
|
130
|
+
route: accountRoute,
|
|
131
|
+
result: { status: 200 },
|
|
132
|
+
});
|
|
133
|
+
assert.match(String(tamperedCommit.setCookie), /^webstir_session=;.*Max-Age=0/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('resetInMemorySessionStore clears an injected in-memory store', () => {
|
|
137
|
+
const store = createInMemorySessionStore();
|
|
138
|
+
|
|
139
|
+
const created = prepareSessionState({
|
|
140
|
+
cookies: '',
|
|
141
|
+
route: loginRoute,
|
|
142
|
+
config,
|
|
143
|
+
store,
|
|
144
|
+
});
|
|
145
|
+
const createdCommit = created.commit({
|
|
146
|
+
session: {
|
|
147
|
+
userId: 'ada@example.com',
|
|
148
|
+
},
|
|
149
|
+
route: loginRoute,
|
|
150
|
+
result: {
|
|
151
|
+
status: 303,
|
|
152
|
+
redirect: { location: '/session/account' },
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const cookieHeader = extractCookieHeader(createdCommit.setCookie);
|
|
156
|
+
|
|
157
|
+
resetInMemorySessionStore(store);
|
|
158
|
+
|
|
159
|
+
const afterReset = prepareSessionState({
|
|
160
|
+
cookies: cookieHeader,
|
|
161
|
+
route: accountRoute,
|
|
162
|
+
config,
|
|
163
|
+
store,
|
|
164
|
+
});
|
|
165
|
+
assert.equal(afterReset.session, null);
|
|
166
|
+
assert.deepEqual(afterReset.flash, []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('prepareSessionState preserves session ids for updates and rotates after clearing', () => {
|
|
170
|
+
const store = createInMemorySessionStore();
|
|
171
|
+
const created = prepareSessionState({
|
|
172
|
+
cookies: '',
|
|
173
|
+
route: loginRoute,
|
|
174
|
+
config,
|
|
175
|
+
store,
|
|
176
|
+
});
|
|
177
|
+
const createdCommit = created.commit({
|
|
178
|
+
session: {
|
|
179
|
+
userId: 'ada@example.com',
|
|
180
|
+
},
|
|
181
|
+
route: loginRoute,
|
|
182
|
+
result: {
|
|
183
|
+
status: 303,
|
|
184
|
+
redirect: { location: '/session/account' },
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const firstCookie = extractCookieHeader(createdCommit.setCookie);
|
|
188
|
+
const firstId = extractSessionId(firstCookie, config.cookieName);
|
|
189
|
+
|
|
190
|
+
const read = prepareSessionState({
|
|
191
|
+
cookies: firstCookie,
|
|
192
|
+
route: accountRoute,
|
|
193
|
+
config,
|
|
194
|
+
store,
|
|
195
|
+
});
|
|
196
|
+
const updatedCommit = read.commit({
|
|
197
|
+
session: {
|
|
198
|
+
...read.session,
|
|
199
|
+
theme: 'dark',
|
|
200
|
+
},
|
|
201
|
+
route: accountRoute,
|
|
202
|
+
result: { status: 200 },
|
|
203
|
+
});
|
|
204
|
+
assert.equal(updatedCommit.setCookie, undefined);
|
|
205
|
+
assert.ok(store.get(firstId), 'expected ordinary session updates to keep the current id');
|
|
206
|
+
|
|
207
|
+
const cleared = prepareSessionState({
|
|
208
|
+
cookies: firstCookie,
|
|
209
|
+
route: accountRoute,
|
|
210
|
+
config,
|
|
211
|
+
store,
|
|
212
|
+
});
|
|
213
|
+
const clearedCommit = cleared.commit({
|
|
214
|
+
session: null,
|
|
215
|
+
route: accountRoute,
|
|
216
|
+
result: { status: 303, redirect: { location: '/signed-out' } },
|
|
217
|
+
});
|
|
218
|
+
assert.match(String(clearedCommit.setCookie), /^webstir_session=;.*Max-Age=0/);
|
|
219
|
+
assert.equal(store.get(firstId), undefined);
|
|
220
|
+
|
|
221
|
+
const recreated = prepareSessionState({
|
|
222
|
+
cookies: '',
|
|
223
|
+
route: loginRoute,
|
|
224
|
+
config,
|
|
225
|
+
store,
|
|
226
|
+
});
|
|
227
|
+
const recreatedCommit = recreated.commit({
|
|
228
|
+
session: {
|
|
229
|
+
userId: 'ada@example.com',
|
|
230
|
+
},
|
|
231
|
+
route: loginRoute,
|
|
232
|
+
result: { status: 303, redirect: { location: '/session/account' } },
|
|
233
|
+
});
|
|
234
|
+
const nextId = extractSessionId(
|
|
235
|
+
extractCookieHeader(recreatedCommit.setCookie),
|
|
236
|
+
config.cookieName,
|
|
237
|
+
);
|
|
238
|
+
assert.notEqual(nextId, firstId);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('processFormSubmission consumes valid csrf tokens so replay fails with retry state', () => {
|
|
242
|
+
const route = {
|
|
243
|
+
path: '/account/settings',
|
|
244
|
+
form: { csrf: true },
|
|
245
|
+
};
|
|
246
|
+
const page = prepareFormState({
|
|
247
|
+
session: null,
|
|
248
|
+
formId: 'account-settings',
|
|
249
|
+
route,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const validationFailure = processFormSubmission({
|
|
253
|
+
session: page.session,
|
|
254
|
+
body: {
|
|
255
|
+
_csrf: page.csrfToken,
|
|
256
|
+
email: 'invalid-email',
|
|
257
|
+
},
|
|
258
|
+
auth: { source: 'service-token' },
|
|
259
|
+
formId: 'account-settings',
|
|
260
|
+
route,
|
|
261
|
+
redirectTo: route.path,
|
|
262
|
+
validate(values) {
|
|
263
|
+
return typeof values.email === 'string' && values.email.includes('@')
|
|
264
|
+
? []
|
|
265
|
+
: [{ field: 'email', message: 'Enter a valid email address.' }];
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
assert.equal(validationFailure.ok, false);
|
|
269
|
+
|
|
270
|
+
const replay = processFormSubmission({
|
|
271
|
+
session: validationFailure.session,
|
|
272
|
+
body: {
|
|
273
|
+
_csrf: page.csrfToken,
|
|
274
|
+
email: 'ada@example.com',
|
|
275
|
+
},
|
|
276
|
+
auth: { source: 'service-token' },
|
|
277
|
+
formId: 'account-settings',
|
|
278
|
+
route,
|
|
279
|
+
redirectTo: route.path,
|
|
280
|
+
});
|
|
281
|
+
assert.equal(replay.ok, false);
|
|
282
|
+
assert.deepEqual(replay.issues, [
|
|
283
|
+
{
|
|
284
|
+
code: 'csrf',
|
|
285
|
+
message: 'Form session expired. Reload the page and try again.',
|
|
286
|
+
},
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const retryPage = prepareFormState({
|
|
290
|
+
session: replay.session,
|
|
291
|
+
formId: 'account-settings',
|
|
292
|
+
route,
|
|
293
|
+
});
|
|
294
|
+
assert.equal(retryPage.values.email, 'ada@example.com');
|
|
295
|
+
assert.deepEqual(retryPage.issues, [
|
|
296
|
+
{
|
|
297
|
+
code: 'csrf',
|
|
298
|
+
message: 'Form session expired. Reload the page and try again.',
|
|
299
|
+
},
|
|
300
|
+
]);
|
|
301
|
+
assert.notEqual(retryPage.csrfToken, page.csrfToken);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('prepareSessionState migrates legacy embedded form runtime without leaking the old payload key', () => {
|
|
305
|
+
const store = createInMemorySessionStore();
|
|
306
|
+
const created = prepareSessionState({
|
|
307
|
+
cookies: '',
|
|
308
|
+
route: loginRoute,
|
|
309
|
+
config,
|
|
310
|
+
store,
|
|
311
|
+
});
|
|
312
|
+
const createdCommit = created.commit({
|
|
313
|
+
session: {
|
|
314
|
+
userId: 'ada@example.com',
|
|
315
|
+
},
|
|
316
|
+
route: loginRoute,
|
|
317
|
+
result: {
|
|
318
|
+
status: 303,
|
|
319
|
+
redirect: { location: '/session/account' },
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
const cookieHeader = extractCookieHeader(createdCommit.setCookie);
|
|
323
|
+
const sessionId = extractSessionId(cookieHeader, config.cookieName);
|
|
324
|
+
const existing = store.get(sessionId);
|
|
325
|
+
|
|
326
|
+
assert.ok(existing, 'expected stored session record');
|
|
327
|
+
|
|
328
|
+
store.set({
|
|
329
|
+
...existing,
|
|
330
|
+
value: {
|
|
331
|
+
...existing.value,
|
|
332
|
+
__webstir_form_runtime: {
|
|
333
|
+
csrf: {
|
|
334
|
+
profile: 'csrf-token-123',
|
|
335
|
+
},
|
|
336
|
+
states: {
|
|
337
|
+
profile: {
|
|
338
|
+
values: {
|
|
339
|
+
email: 'ada@example.com',
|
|
340
|
+
},
|
|
341
|
+
issues: [
|
|
342
|
+
{
|
|
343
|
+
code: 'validation',
|
|
344
|
+
field: 'email',
|
|
345
|
+
message: 'Enter a valid email address.',
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
createdAt: existing.createdAt,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
runtime: undefined,
|
|
354
|
+
flash: [],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const read = prepareSessionState({
|
|
358
|
+
cookies: cookieHeader,
|
|
359
|
+
config,
|
|
360
|
+
store,
|
|
361
|
+
});
|
|
362
|
+
assert.equal(read.session?.userId, 'ada@example.com');
|
|
363
|
+
assert.equal(Object.hasOwn(read.session ?? {}, '__webstir_form_runtime'), false);
|
|
364
|
+
|
|
365
|
+
const page = prepareFormState({
|
|
366
|
+
session: read.session,
|
|
367
|
+
formId: 'profile',
|
|
368
|
+
route: {
|
|
369
|
+
path: '/account/settings',
|
|
370
|
+
form: { csrf: true },
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
assert.equal(page.values.email, 'ada@example.com');
|
|
375
|
+
assert.deepEqual(page.issues, [
|
|
376
|
+
{
|
|
377
|
+
code: 'validation',
|
|
378
|
+
field: 'email',
|
|
379
|
+
message: 'Enter a valid email address.',
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
assert.equal(page.csrfToken, 'csrf-token-123');
|
|
383
|
+
assert.equal(Object.hasOwn(page.session, '__webstir_form_runtime'), false);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('prepareSessionState keeps session metadata accessible without persisting it inside the app payload', () => {
|
|
387
|
+
const store = createInMemorySessionStore();
|
|
388
|
+
const created = prepareSessionState({
|
|
389
|
+
cookies: '',
|
|
390
|
+
route: loginRoute,
|
|
391
|
+
config,
|
|
392
|
+
store,
|
|
393
|
+
});
|
|
394
|
+
const createdCommit = created.commit({
|
|
395
|
+
session: {
|
|
396
|
+
userId: 'ada@example.com',
|
|
397
|
+
},
|
|
398
|
+
route: loginRoute,
|
|
399
|
+
result: {
|
|
400
|
+
status: 303,
|
|
401
|
+
redirect: { location: '/session/account' },
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
const cookieHeader = extractCookieHeader(createdCommit.setCookie);
|
|
405
|
+
const sessionId = extractSessionId(cookieHeader, config.cookieName);
|
|
406
|
+
const stored = store.get(sessionId);
|
|
407
|
+
|
|
408
|
+
assert.ok(stored, 'expected stored session record');
|
|
409
|
+
assert.equal(Object.hasOwn(stored.value, 'id'), false);
|
|
410
|
+
assert.equal(Object.hasOwn(stored.value, 'createdAt'), false);
|
|
411
|
+
assert.equal(Object.hasOwn(stored.value, 'expiresAt'), false);
|
|
412
|
+
assert.equal(stored.id, sessionId);
|
|
413
|
+
|
|
414
|
+
const read = prepareSessionState({
|
|
415
|
+
cookies: cookieHeader,
|
|
416
|
+
route: accountRoute,
|
|
417
|
+
config,
|
|
418
|
+
store,
|
|
419
|
+
});
|
|
420
|
+
assert.equal(read.session?.id, sessionId);
|
|
421
|
+
assert.match(String(read.session?.createdAt), /^\d{4}-\d{2}-\d{2}T/);
|
|
422
|
+
assert.match(String(read.session?.expiresAt), /^\d{4}-\d{2}-\d{2}T/);
|
|
423
|
+
assert.equal(Object.hasOwn(read.session ?? {}, 'id'), true);
|
|
424
|
+
assert.equal(Object.keys(read.session ?? {}).includes('id'), false);
|
|
425
|
+
assert.equal(Object.keys(read.session ?? {}).includes('createdAt'), false);
|
|
426
|
+
assert.equal(Object.keys(read.session ?? {}).includes('expiresAt'), false);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('prepareSessionState stores flash in runtime metadata while preserving legacy top-level flash reads', () => {
|
|
430
|
+
const store = createInMemorySessionStore();
|
|
431
|
+
const created = prepareSessionState({
|
|
432
|
+
cookies: '',
|
|
433
|
+
route: loginRoute,
|
|
434
|
+
config,
|
|
435
|
+
store,
|
|
436
|
+
});
|
|
437
|
+
const createdCommit = created.commit({
|
|
438
|
+
session: {
|
|
439
|
+
userId: 'ada@example.com',
|
|
440
|
+
},
|
|
441
|
+
route: loginRoute,
|
|
442
|
+
result: {
|
|
443
|
+
status: 303,
|
|
444
|
+
redirect: { location: '/session/account' },
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
const cookieHeader = extractCookieHeader(createdCommit.setCookie);
|
|
448
|
+
const sessionId = extractSessionId(cookieHeader, config.cookieName);
|
|
449
|
+
const stored = store.get(sessionId);
|
|
450
|
+
|
|
451
|
+
assert.ok(stored, 'expected stored session record');
|
|
452
|
+
assert.equal(Object.hasOwn(stored, 'flash'), false);
|
|
453
|
+
assert.deepEqual(
|
|
454
|
+
(stored.runtime?.flash ?? []).map((message) => ({ key: message.key, level: message.level })),
|
|
455
|
+
[{ key: 'signed-in', level: 'success' }],
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
store.set({
|
|
459
|
+
...stored,
|
|
460
|
+
flash: stored.runtime?.flash ?? [],
|
|
461
|
+
runtime: undefined,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const read = prepareSessionState({
|
|
465
|
+
cookies: cookieHeader,
|
|
466
|
+
route: accountRoute,
|
|
467
|
+
config,
|
|
468
|
+
store,
|
|
469
|
+
});
|
|
470
|
+
assert.equal(read.session?.userId, 'ada@example.com');
|
|
471
|
+
assert.deepEqual(
|
|
472
|
+
read.flash.map((message) => ({ key: message.key, level: message.level })),
|
|
473
|
+
[{ key: 'signed-in', level: 'success' }],
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
function extractCookieHeader(setCookie) {
|
|
478
|
+
assert.ok(setCookie, 'expected a session cookie');
|
|
479
|
+
return String(setCookie).split(';')[0];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function extractSessionId(cookieHeader, cookieName) {
|
|
483
|
+
const [nameValue] = String(cookieHeader).split(';');
|
|
484
|
+
const prefix = `${cookieName}=`;
|
|
485
|
+
assert.ok(nameValue.startsWith(prefix), `expected ${cookieName} cookie`);
|
|
486
|
+
const encodedValue = nameValue.slice(prefix.length);
|
|
487
|
+
const separatorIndex = encodedValue.indexOf('.');
|
|
488
|
+
assert.notEqual(separatorIndex, -1, 'expected signed session cookie');
|
|
489
|
+
return decodeURIComponent(encodedValue.slice(0, separatorIndex));
|
|
490
|
+
}
|