@prairielearn/session 1.0.0
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/.turbo/turbo-build.log +0 -0
- package/README.md +39 -0
- package/dist/before-end.d.ts +25 -0
- package/dist/before-end.js +81 -0
- package/dist/before-end.js.map +1 -0
- package/dist/before-end.test.d.ts +1 -0
- package/dist/before-end.test.js +33 -0
- package/dist/before-end.test.js.map +1 -0
- package/dist/cookie.d.ts +4 -0
- package/dist/cookie.js +34 -0
- package/dist/cookie.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +506 -0
- package/dist/index.test.js.map +1 -0
- package/dist/memory-store.d.ts +7 -0
- package/dist/memory-store.js +28 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/session.d.ts +14 -0
- package/dist/session.js +78 -0
- package/dist/session.js.map +1 -0
- package/dist/session.test.d.ts +1 -0
- package/dist/session.test.js +92 -0
- package/dist/session.test.js.map +1 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +3 -0
- package/dist/store.js.map +1 -0
- package/dist/test-utils.d.ts +10 -0
- package/dist/test-utils.js +32 -0
- package/dist/test-utils.js.map +1 -0
- package/package.json +44 -0
- package/src/before-end.test.ts +34 -0
- package/src/before-end.ts +96 -0
- package/src/cookie.ts +38 -0
- package/src/index.test.ts +628 -0
- package/src/index.ts +132 -0
- package/src/memory-store.ts +25 -0
- package/src/session.test.ts +122 -0
- package/src/session.ts +106 -0
- package/src/store.ts +10 -0
- package/src/test-utils.ts +42 -0
- package/tsconfig.json +11 -0
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# `@prairielearn/session`
|
|
2
|
+
|
|
3
|
+
The implementation borrows heavily from prior art such as [`express-session`](https://github.com/expressjs/session) and [`fastify-session`](https://github.com/fastify/session). However, the semantics and functionality have been changed to better suit PrairieLearn's needs. Specifically:
|
|
4
|
+
|
|
5
|
+
- We need to have more precise control over when the session is written back to the session store. `express-session` will try to write the session on every request, which produces an undesirable amount of load on the database.
|
|
6
|
+
- We need to have more precise control over when new/updated cookies are sent back to the client. In the near future, we'll need to avoid writing these cookies when requests are served from subdomains.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import { createSessionMiddleware, MemoryStore } from '@prairielearn/session';
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
|
|
16
|
+
app.use(
|
|
17
|
+
createSessionMiddleware({
|
|
18
|
+
store: new MemoryStore(),
|
|
19
|
+
secret: 'top_secret',
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Controlling when cookies are set
|
|
25
|
+
|
|
26
|
+
You can pass a `canSetCookie` function to `createSessionMiddleware` to provide control over when session cookies will be returned to the client.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
createSessionMiddleware({
|
|
30
|
+
canSetCookie: (req) => {
|
|
31
|
+
return req.hostname === 'us.prairielearn.com';
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This can be useful to enforce that a session cookie is only ever set from a root domain and not subdomains.
|
|
37
|
+
|
|
38
|
+
- If a request is received that does not have a valid session cookie, a temporary session will be created at `req.session`, but it won't be persisted since the client won't know about the session ID.
|
|
39
|
+
- If a request is received that already has a valid session cookie, modifications to the session will be persisted, but the cookie itself won't be updated.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { NextFunction } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* The following function is based on code from `express-session`:
|
|
4
|
+
*
|
|
5
|
+
* https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L246-L360
|
|
6
|
+
*
|
|
7
|
+
* This code is used to work around the fact that Express doesn't have a good
|
|
8
|
+
* hook to allow us to perform some asynchronous operation before the response
|
|
9
|
+
* is written to the client.
|
|
10
|
+
*
|
|
11
|
+
* Note that this is truly only necessary for Express. Other Node frameworks
|
|
12
|
+
* like Fastify and Adonis have hooks that allow us to do this without any
|
|
13
|
+
* hacks. It's also probably only useful in the context of Express, as it
|
|
14
|
+
* seems to rely on the fact that Express and its ecosystem generally don't
|
|
15
|
+
* call `end()` without an additional chunk of data. If it instead called
|
|
16
|
+
* `write()` with the final data and then `end()` with no data, this code
|
|
17
|
+
* wouldn't function as intended. It's possible that `stream.pipe(res)` does
|
|
18
|
+
* in fact behave this way, so it's probably not completely safe to use this
|
|
19
|
+
* code when streaming responses back to the client.
|
|
20
|
+
*
|
|
21
|
+
* One could probably make this safer by *also* hooking into `response.write()`
|
|
22
|
+
* and buffering the data. My understanding of Node streams isn't good enough
|
|
23
|
+
* to implement that, though.
|
|
24
|
+
*/
|
|
25
|
+
export declare function beforeEnd(res: any, next: NextFunction, fn: () => Promise<void>): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.beforeEnd = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The following function is based on code from `express-session`:
|
|
6
|
+
*
|
|
7
|
+
* https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L246-L360
|
|
8
|
+
*
|
|
9
|
+
* This code is used to work around the fact that Express doesn't have a good
|
|
10
|
+
* hook to allow us to perform some asynchronous operation before the response
|
|
11
|
+
* is written to the client.
|
|
12
|
+
*
|
|
13
|
+
* Note that this is truly only necessary for Express. Other Node frameworks
|
|
14
|
+
* like Fastify and Adonis have hooks that allow us to do this without any
|
|
15
|
+
* hacks. It's also probably only useful in the context of Express, as it
|
|
16
|
+
* seems to rely on the fact that Express and its ecosystem generally don't
|
|
17
|
+
* call `end()` without an additional chunk of data. If it instead called
|
|
18
|
+
* `write()` with the final data and then `end()` with no data, this code
|
|
19
|
+
* wouldn't function as intended. It's possible that `stream.pipe(res)` does
|
|
20
|
+
* in fact behave this way, so it's probably not completely safe to use this
|
|
21
|
+
* code when streaming responses back to the client.
|
|
22
|
+
*
|
|
23
|
+
* One could probably make this safer by *also* hooking into `response.write()`
|
|
24
|
+
* and buffering the data. My understanding of Node streams isn't good enough
|
|
25
|
+
* to implement that, though.
|
|
26
|
+
*/
|
|
27
|
+
function beforeEnd(res, next, fn) {
|
|
28
|
+
const _end = res.end;
|
|
29
|
+
const _write = res.write;
|
|
30
|
+
let ended = false;
|
|
31
|
+
res.end = function end(chunk, encoding) {
|
|
32
|
+
if (ended) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
ended = true;
|
|
36
|
+
let ret;
|
|
37
|
+
let sync = true;
|
|
38
|
+
function writeend() {
|
|
39
|
+
if (sync) {
|
|
40
|
+
ret = _end.call(res, chunk, encoding);
|
|
41
|
+
sync = false;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
_end.call(res);
|
|
45
|
+
}
|
|
46
|
+
function writetop() {
|
|
47
|
+
if (!sync) {
|
|
48
|
+
return ret;
|
|
49
|
+
}
|
|
50
|
+
if (!res._header) {
|
|
51
|
+
res._implicitHeader();
|
|
52
|
+
}
|
|
53
|
+
if (chunk == null) {
|
|
54
|
+
ret = true;
|
|
55
|
+
return ret;
|
|
56
|
+
}
|
|
57
|
+
const contentLength = Number(res.getHeader('Content-Length'));
|
|
58
|
+
if (!isNaN(contentLength) && contentLength > 0) {
|
|
59
|
+
chunk = !Buffer.isBuffer(chunk) ? Buffer.from(chunk, encoding) : chunk;
|
|
60
|
+
encoding = undefined;
|
|
61
|
+
if (chunk.length !== 0) {
|
|
62
|
+
ret = _write.call(res, chunk.slice(0, chunk.length - 1));
|
|
63
|
+
chunk = chunk.slice(chunk.length - 1, chunk.length);
|
|
64
|
+
return ret;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
ret = _write.call(res, chunk, encoding);
|
|
68
|
+
sync = false;
|
|
69
|
+
return ret;
|
|
70
|
+
}
|
|
71
|
+
fn().then(() => {
|
|
72
|
+
writeend();
|
|
73
|
+
}, (err) => {
|
|
74
|
+
setImmediate(next, err);
|
|
75
|
+
writeend();
|
|
76
|
+
});
|
|
77
|
+
return writetop();
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
exports.beforeEnd = beforeEnd;
|
|
81
|
+
//# sourceMappingURL=before-end.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"before-end.js","sourceRoot":"","sources":["../src/before-end.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,SAAgB,SAAS,CAAC,GAAQ,EAAE,IAAkB,EAAE,EAAuB;IAC7E,MAAM,IAAI,GAAG,GAAG,CAAC,GAAU,CAAC;IAC5B,MAAM,MAAM,GAAG,GAAG,CAAC,KAAY,CAAC;IAChC,IAAI,KAAK,GAAG,KAAK,CAAC;IAElB,GAAG,CAAC,GAAG,GAAG,SAAS,GAAG,CAAC,KAAU,EAAE,QAAa;QAC9C,IAAI,KAAK,EAAE;YACT,OAAO,KAAK,CAAC;SACd;QAED,KAAK,GAAG,IAAI,CAAC;QAEb,IAAI,GAAQ,CAAC;QACb,IAAI,IAAI,GAAG,IAAI,CAAC;QAEhB,SAAS,QAAQ;YACf,IAAI,IAAI,EAAE;gBACR,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;gBACtC,IAAI,GAAG,KAAK,CAAC;gBACb,OAAO;aACR;YAED,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;QAED,SAAS,QAAQ;YACf,IAAI,CAAC,IAAI,EAAE;gBACT,OAAO,GAAG,CAAC;aACZ;YAED,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE;gBAChB,GAAG,CAAC,eAAe,EAAE,CAAC;aACvB;YAED,IAAI,KAAK,IAAI,IAAI,EAAE;gBACjB,GAAG,GAAG,IAAI,CAAC;gBACX,OAAO,GAAG,CAAC;aACZ;YAED,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAE9D,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,aAAa,GAAG,CAAC,EAAE;gBAC9C,KAAK,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;gBACvE,QAAQ,GAAG,SAAS,CAAC;gBAErB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;oBACtB,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;oBACzD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;oBACpD,OAAO,GAAG,CAAC;iBACZ;aACF;YAED,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;YACxC,IAAI,GAAG,KAAK,CAAC;YAEb,OAAO,GAAG,CAAC;QACb,CAAC;QAED,EAAE,EAAE,CAAC,IAAI,CACP,GAAG,EAAE;YACH,QAAQ,EAAE,CAAC;QACb,CAAC,EACD,CAAC,GAAG,EAAE,EAAE;YACN,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACxB,QAAQ,EAAE,CAAC;QACb,CAAC,CACF,CAAC;QAEF,OAAO,QAAQ,EAAE,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC;AAtED,8BAsEC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const chai_1 = require("chai");
|
|
9
|
+
const before_end_1 = require("./before-end");
|
|
10
|
+
const test_utils_1 = require("./test-utils");
|
|
11
|
+
describe('beforeEnd', () => {
|
|
12
|
+
it('handles errors correctly', async () => {
|
|
13
|
+
const app = (0, express_1.default)();
|
|
14
|
+
app.use((_req, res, next) => {
|
|
15
|
+
(0, before_end_1.beforeEnd)(res, next, async () => {
|
|
16
|
+
throw new Error('oops');
|
|
17
|
+
});
|
|
18
|
+
next();
|
|
19
|
+
});
|
|
20
|
+
app.get('/', (_req, res) => res.sendStatus(200));
|
|
21
|
+
let error = null;
|
|
22
|
+
app.use((err, _req, _res, next) => {
|
|
23
|
+
error = err;
|
|
24
|
+
next();
|
|
25
|
+
});
|
|
26
|
+
await (0, test_utils_1.withServer)(app, async ({ url }) => {
|
|
27
|
+
const res = await (0, node_fetch_1.default)(url);
|
|
28
|
+
chai_1.assert.equal(res.status, 200);
|
|
29
|
+
chai_1.assert.equal(error?.message, 'oops');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=before-end.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"before-end.test.js","sourceRoot":"","sources":["../src/before-end.test.ts"],"names":[],"mappings":";;;;;AAAA,sDAAkF;AAClF,4DAA+B;AAC/B,+BAA8B;AAE9B,6CAAyC;AACzC,6CAA0C;AAE1C,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;QACxC,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YAC1B,IAAA,sBAAS,EAAC,GAAG,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE;gBAC9B,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;YAC1B,CAAC,CAAC,CAAC;YAEH,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAEjD,IAAI,KAAK,GAAiB,IAAI,CAAC;QAC/B,GAAG,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,IAAa,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;YACtE,KAAK,GAAG,GAAG,CAAC;YACZ,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;QAEH,MAAM,IAAA,uBAAU,EAAC,GAAG,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACtC,MAAM,GAAG,GAAG,MAAM,IAAA,oBAAK,EAAC,GAAG,CAAC,CAAC;YAE7B,aAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC9B,aAAM,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/cookie.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
export type CookieSecure = boolean | 'auto' | ((req: Request) => boolean);
|
|
3
|
+
export declare function shouldSecureCookie(req: Request, secure: CookieSecure): boolean;
|
|
4
|
+
export declare function getSessionIdFromCookie(req: Request, cookieName: string, secrets: string[]): string | null;
|
package/dist/cookie.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getSessionIdFromCookie = exports.shouldSecureCookie = void 0;
|
|
7
|
+
const cookie_1 = __importDefault(require("cookie"));
|
|
8
|
+
const cookie_signature_1 = __importDefault(require("cookie-signature"));
|
|
9
|
+
function shouldSecureCookie(req, secure) {
|
|
10
|
+
if (typeof secure === 'function') {
|
|
11
|
+
return secure(req);
|
|
12
|
+
}
|
|
13
|
+
if (secure === 'auto') {
|
|
14
|
+
return req.protocol === 'https';
|
|
15
|
+
}
|
|
16
|
+
return secure;
|
|
17
|
+
}
|
|
18
|
+
exports.shouldSecureCookie = shouldSecureCookie;
|
|
19
|
+
function getSessionIdFromCookie(req, cookieName, secrets) {
|
|
20
|
+
const cookies = cookie_1.default.parse(req.headers.cookie ?? '');
|
|
21
|
+
const sessionCookie = cookies[cookieName];
|
|
22
|
+
if (!sessionCookie)
|
|
23
|
+
return null;
|
|
24
|
+
// Try each secret until we find one that works.
|
|
25
|
+
for (const secret of secrets) {
|
|
26
|
+
const value = cookie_signature_1.default.unsign(sessionCookie, secret);
|
|
27
|
+
if (value !== false) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
exports.getSessionIdFromCookie = getSessionIdFromCookie;
|
|
34
|
+
//# sourceMappingURL=cookie.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie.js","sourceRoot":"","sources":["../src/cookie.ts"],"names":[],"mappings":";;;;;;AAAA,oDAA4B;AAC5B,wEAAyC;AAKzC,SAAgB,kBAAkB,CAAC,GAAY,EAAE,MAAoB;IACnE,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE;QAChC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;KACpB;IAED,IAAI,MAAM,KAAK,MAAM,EAAE;QACrB,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC;KACjC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAVD,gDAUC;AAED,SAAgB,sBAAsB,CACpC,GAAY,EACZ,UAAkB,EAClB,OAAiB;IAEjB,MAAM,OAAO,GAAG,gBAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IACvD,MAAM,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAE1C,IAAI,CAAC,aAAa;QAAE,OAAO,IAAI,CAAC;IAEhC,gDAAgD;IAChD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;QAC5B,MAAM,KAAK,GAAG,0BAAS,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;QACtD,IAAI,KAAK,KAAK,KAAK,EAAE;YACnB,OAAO,KAAK,CAAC;SACd;KACF;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAnBD,wDAmBC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import { SessionStore } from './store';
|
|
3
|
+
import { type CookieSecure } from './cookie';
|
|
4
|
+
import { type Session } from './session';
|
|
5
|
+
declare global {
|
|
6
|
+
namespace Express {
|
|
7
|
+
interface Request {
|
|
8
|
+
session: Session;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export interface SessionOptions {
|
|
13
|
+
secret: string | string[];
|
|
14
|
+
store: SessionStore;
|
|
15
|
+
canSetCookie?: (req: Request) => boolean;
|
|
16
|
+
cookie?: {
|
|
17
|
+
name?: string;
|
|
18
|
+
secure?: CookieSecure;
|
|
19
|
+
httpOnly?: boolean;
|
|
20
|
+
sameSite?: boolean | 'none' | 'lax' | 'strict';
|
|
21
|
+
maxAge?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function createSessionMiddleware(options: SessionOptions): import("express").RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createSessionMiddleware = void 0;
|
|
7
|
+
const on_headers_1 = __importDefault(require("on-headers"));
|
|
8
|
+
const cookie_signature_1 = __importDefault(require("cookie-signature"));
|
|
9
|
+
const express_async_handler_1 = __importDefault(require("express-async-handler"));
|
|
10
|
+
const before_end_1 = require("./before-end");
|
|
11
|
+
const cookie_1 = require("./cookie");
|
|
12
|
+
const session_1 = require("./session");
|
|
13
|
+
const DEFAULT_COOKIE_NAME = 'session';
|
|
14
|
+
const DEFAULT_COOKIE_MAX_AGE = 86400000; // 1 day
|
|
15
|
+
function createSessionMiddleware(options) {
|
|
16
|
+
const secrets = Array.isArray(options.secret) ? options.secret : [options.secret];
|
|
17
|
+
const cookieName = options.cookie?.name ?? DEFAULT_COOKIE_NAME;
|
|
18
|
+
const cookieMaxAge = options.cookie?.maxAge ?? DEFAULT_COOKIE_MAX_AGE;
|
|
19
|
+
const store = options.store;
|
|
20
|
+
return (0, express_async_handler_1.default)(async function sessionMiddleware(req, res, next) {
|
|
21
|
+
const cookieSessionId = (0, cookie_1.getSessionIdFromCookie)(req, cookieName, secrets);
|
|
22
|
+
const sessionId = cookieSessionId ?? (await (0, session_1.generateSessionId)());
|
|
23
|
+
req.session = await (0, session_1.loadSession)(sessionId, req, store, cookieMaxAge);
|
|
24
|
+
const originalHash = (0, session_1.hashSession)(req.session);
|
|
25
|
+
const originalExpirationDate = req.session.getExpirationDate();
|
|
26
|
+
const canSetCookie = options.canSetCookie?.(req) ?? true;
|
|
27
|
+
(0, on_headers_1.default)(res, () => {
|
|
28
|
+
if (!req.session) {
|
|
29
|
+
if (cookieSessionId) {
|
|
30
|
+
// If the request arrived with a session cookie but the session was
|
|
31
|
+
// destroyed, clear the cookie.
|
|
32
|
+
res.clearCookie(cookieName);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// There is no session to do anything with.
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const secureCookie = (0, cookie_1.shouldSecureCookie)(req, options.cookie?.secure ?? 'auto');
|
|
39
|
+
if (secureCookie && req.protocol !== 'https') {
|
|
40
|
+
// Avoid sending cookie over insecure connection.
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const isNewSession = !cookieSessionId || cookieSessionId !== req.session.id;
|
|
44
|
+
const didExpirationChange = originalExpirationDate.getTime() !== req.session.getExpirationDate().getTime();
|
|
45
|
+
if (canSetCookie && (isNewSession || didExpirationChange)) {
|
|
46
|
+
const signedSessionId = signSessionId(req.session.id, secrets[0]);
|
|
47
|
+
res.cookie(cookieName, signedSessionId, {
|
|
48
|
+
secure: secureCookie,
|
|
49
|
+
httpOnly: options.cookie?.httpOnly ?? true,
|
|
50
|
+
sameSite: options.cookie?.sameSite ?? false,
|
|
51
|
+
expires: req.session.getExpirationDate(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
(0, before_end_1.beforeEnd)(res, next, async () => {
|
|
56
|
+
if (!req.session) {
|
|
57
|
+
// There is no session to do anything with.
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const isExistingSession = cookieSessionId && cookieSessionId === req.session.id;
|
|
61
|
+
const hashChanged = (0, session_1.hashSession)(req.session) !== originalHash;
|
|
62
|
+
const didExpirationChange = originalExpirationDate.getTime() !== req.session.getExpirationDate().getTime();
|
|
63
|
+
if ((hashChanged && isExistingSession) ||
|
|
64
|
+
(canSetCookie && (!isExistingSession || didExpirationChange))) {
|
|
65
|
+
// Only update the expiration date in the store if we were actually
|
|
66
|
+
// able to update the cookie too.
|
|
67
|
+
const expirationDate = canSetCookie
|
|
68
|
+
? req.session.getExpirationDate()
|
|
69
|
+
: originalExpirationDate;
|
|
70
|
+
await store.set(req.session.id, req.session,
|
|
71
|
+
// Cookies only support second-level resolution. To ensure consistency
|
|
72
|
+
// between the cookie and the store, truncate the expiration date to
|
|
73
|
+
// the nearest second.
|
|
74
|
+
truncateExpirationDate(expirationDate));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
next();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
exports.createSessionMiddleware = createSessionMiddleware;
|
|
81
|
+
function signSessionId(sessionId, secret) {
|
|
82
|
+
return cookie_signature_1.default.sign(sessionId, secret);
|
|
83
|
+
}
|
|
84
|
+
function truncateExpirationDate(date) {
|
|
85
|
+
const time = date.getTime();
|
|
86
|
+
const truncatedTime = Math.floor(time / 1000) * 1000;
|
|
87
|
+
return new Date(truncatedTime);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|