@prairielearn/session 2.0.6 → 3.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/.mocharc.cjs +3 -0
- package/CHANGELOG.md +6 -0
- package/README.md +18 -3
- package/dist/before-end.js +1 -5
- package/dist/before-end.js.map +1 -1
- package/dist/before-end.test.js +11 -16
- package/dist/before-end.test.js.map +1 -1
- package/dist/cookie.d.ts +0 -4
- package/dist/cookie.js +4 -33
- package/dist/cookie.js.map +1 -1
- package/dist/index.d.ts +25 -14
- package/dist/index.js +55 -47
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +213 -210
- package/dist/index.test.js.map +1 -1
- package/dist/memory-store.d.ts +1 -1
- package/dist/memory-store.js +1 -5
- package/dist/memory-store.js.map +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +9 -20
- package/dist/session.js.map +1 -1
- package/dist/session.test.js +35 -37
- package/dist/session.test.js.map +1 -1
- package/dist/store.js +1 -2
- package/dist/store.js.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.js +1 -5
- package/dist/test-utils.js.map +1 -1
- package/package.json +5 -4
- package/src/before-end.test.ts +1 -1
- package/src/cookie.ts +0 -23
- package/src/index.test.ts +15 -6
- package/src/index.ts +69 -46
- package/src/memory-store.ts +1 -1
- package/src/session.test.ts +2 -2
- package/src/session.ts +1 -1
package/.mocharc.cjs
ADDED
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -23,15 +23,30 @@ app.use(
|
|
|
23
23
|
|
|
24
24
|
### Rotate session cookies
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
It can be useful to rotate to a new session cookie name. For instance, this can be used to provide an explicit subdomain when none was set before.
|
|
27
|
+
|
|
28
|
+
To do this, you can use a combination of `cookie.writeNames` and `cookie.writeOverrides`:
|
|
27
29
|
|
|
28
30
|
```ts
|
|
29
31
|
createSessionMiddleware({
|
|
30
32
|
// ...
|
|
31
33
|
cookie: {
|
|
32
|
-
name:
|
|
34
|
+
name: 'legacy_session',
|
|
35
|
+
writeNames: ['legacy_session', 'session'],
|
|
36
|
+
writeOverrides: [{ domain: undefined }, { domain: '.example.com' }],
|
|
33
37
|
},
|
|
34
38
|
});
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
In this example, the session will be loaded from and persisted to the `legacy_session` cookie. However, when the session is persisted, it will also be written to a new cookie named `session`. The `domain` attribute of the `legacy_session` cookie will not be set, while the `domain` attribute of the `session` cookie will be set to `.example.com`.
|
|
42
|
+
|
|
43
|
+
After this code has been running in production for a while, it will be safe to switch to reading from and writing to the new `session` cookie exclusively:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
createSessionMiddleware({
|
|
47
|
+
cookie: {
|
|
48
|
+
name: 'session',
|
|
49
|
+
domain: '.example.com',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
package/dist/before-end.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.beforeEnd = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* The following function is based on code from `express-session`:
|
|
6
3
|
*
|
|
@@ -24,7 +21,7 @@ exports.beforeEnd = void 0;
|
|
|
24
21
|
* and buffering the data. My understanding of Node streams isn't good enough
|
|
25
22
|
* to implement that, though.
|
|
26
23
|
*/
|
|
27
|
-
function beforeEnd(res, next, fn) {
|
|
24
|
+
export function beforeEnd(res, next, fn) {
|
|
28
25
|
const _end = res.end;
|
|
29
26
|
const _write = res.write;
|
|
30
27
|
let ended = false;
|
|
@@ -77,5 +74,4 @@ function beforeEnd(res, next, fn) {
|
|
|
77
74
|
return writetop();
|
|
78
75
|
};
|
|
79
76
|
}
|
|
80
|
-
exports.beforeEnd = beforeEnd;
|
|
81
77
|
//# sourceMappingURL=before-end.js.map
|
package/dist/before-end.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"before-end.js","sourceRoot":"","sources":["../src/before-end.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"before-end.js","sourceRoot":"","sources":["../src/before-end.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,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,CAAC;YACV,OAAO,KAAK,CAAC;QACf,CAAC;QAED,KAAK,GAAG,IAAI,CAAC;QAEb,IAAI,GAAQ,CAAC;QACb,IAAI,IAAI,GAAG,IAAI,CAAC;QAEhB,SAAS,QAAQ;YACf,IAAI,IAAI,EAAE,CAAC;gBACT,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;gBACtC,IAAI,GAAG,KAAK,CAAC;gBACb,OAAO;YACT,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;QAED,SAAS,QAAQ;YACf,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,GAAG,CAAC;YACb,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;gBACjB,GAAG,CAAC,eAAe,EAAE,CAAC;YACxB,CAAC;YAED,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,GAAG,GAAG,IAAI,CAAC;gBACX,OAAO,GAAG,CAAC;YACb,CAAC;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,CAAC;gBAC/C,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,CAAC;oBACvB,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;gBACb,CAAC;YACH,CAAC;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","sourcesContent":["import type { NextFunction } from 'express';\n\n/**\n * The following function is based on code from `express-session`:\n *\n * https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L246-L360\n *\n * This code is used to work around the fact that Express doesn't have a good\n * hook to allow us to perform some asynchronous operation before the response\n * is written to the client.\n *\n * Note that this is truly only necessary for Express. Other Node frameworks\n * like Fastify and Adonis have hooks that allow us to do this without any\n * hacks. It's also probably only useful in the context of Express, as it\n * seems to rely on the fact that Express and its ecosystem generally don't\n * call `end()` without an additional chunk of data. If it instead called\n * `write()` with the final data and then `end()` with no data, this code\n * wouldn't function as intended. It's possible that `stream.pipe(res)` does\n * in fact behave this way, so it's probably not completely safe to use this\n * code when streaming responses back to the client.\n *\n * One could probably make this safer by *also* hooking into `response.write()`\n * and buffering the data. My understanding of Node streams isn't good enough\n * to implement that, though.\n */\nexport function beforeEnd(res: any, next: NextFunction, fn: () => Promise<void>) {\n const _end = res.end as any;\n const _write = res.write as any;\n let ended = false;\n\n res.end = function end(chunk: any, encoding: any) {\n if (ended) {\n return false;\n }\n\n ended = true;\n\n let ret: any;\n let sync = true;\n\n function writeend() {\n if (sync) {\n ret = _end.call(res, chunk, encoding);\n sync = false;\n return;\n }\n\n _end.call(res);\n }\n\n function writetop() {\n if (!sync) {\n return ret;\n }\n\n if (!res._header) {\n res._implicitHeader();\n }\n\n if (chunk == null) {\n ret = true;\n return ret;\n }\n\n const contentLength = Number(res.getHeader('Content-Length'));\n\n if (!isNaN(contentLength) && contentLength > 0) {\n chunk = !Buffer.isBuffer(chunk) ? Buffer.from(chunk, encoding) : chunk;\n encoding = undefined;\n\n if (chunk.length !== 0) {\n ret = _write.call(res, chunk.slice(0, chunk.length - 1));\n chunk = chunk.slice(chunk.length - 1, chunk.length);\n return ret;\n }\n }\n\n ret = _write.call(res, chunk, encoding);\n sync = false;\n\n return ret;\n }\n\n fn().then(\n () => {\n writeend();\n },\n (err) => {\n setImmediate(next, err);\n writeend();\n },\n );\n\n return writetop();\n };\n}\n"]}
|
package/dist/before-end.test.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
const express_1 = __importDefault(require("express"));
|
|
7
|
-
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
-
const chai_1 = require("chai");
|
|
9
|
-
const express_test_utils_1 = require("@prairielearn/express-test-utils");
|
|
10
|
-
const before_end_1 = require("./before-end");
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import { assert } from 'chai';
|
|
4
|
+
import { withServer } from '@prairielearn/express-test-utils';
|
|
5
|
+
import { beforeEnd } from './before-end.js';
|
|
11
6
|
describe('beforeEnd', () => {
|
|
12
7
|
it('handles errors correctly', async () => {
|
|
13
|
-
const app = (
|
|
8
|
+
const app = express();
|
|
14
9
|
app.use((_req, res, next) => {
|
|
15
|
-
|
|
10
|
+
beforeEnd(res, next, async () => {
|
|
16
11
|
throw new Error('oops');
|
|
17
12
|
});
|
|
18
13
|
next();
|
|
@@ -23,10 +18,10 @@ describe('beforeEnd', () => {
|
|
|
23
18
|
error = err;
|
|
24
19
|
next();
|
|
25
20
|
});
|
|
26
|
-
await
|
|
27
|
-
const res = await (
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
await withServer(app, async ({ url }) => {
|
|
22
|
+
const res = await fetch(url);
|
|
23
|
+
assert.equal(res.status, 200);
|
|
24
|
+
assert.equal(error?.message, 'oops');
|
|
30
25
|
});
|
|
31
26
|
});
|
|
32
27
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"before-end.test.js","sourceRoot":"","sources":["../src/before-end.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"before-end.test.js","sourceRoot":"","sources":["../src/before-end.test.ts"],"names":[],"mappings":"AAAA,OAAO,OAA2D,MAAM,SAAS,CAAC;AAClF,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAE9D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;QACxC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YAC1B,SAAS,CAAC,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,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACtC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;YAE7B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC9B,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import express, { type Request, type Response, type NextFunction } from 'express';\nimport fetch from 'node-fetch';\nimport { assert } from 'chai';\nimport { withServer } from '@prairielearn/express-test-utils';\n\nimport { beforeEnd } from './before-end.js';\n\ndescribe('beforeEnd', () => {\n it('handles errors correctly', async () => {\n const app = express();\n app.use((_req, res, next) => {\n beforeEnd(res, next, async () => {\n throw new Error('oops');\n });\n\n next();\n });\n\n app.get('/', (_req, res) => res.sendStatus(200));\n\n let error: Error | null = null;\n app.use((err: any, _req: Request, _res: Response, next: NextFunction) => {\n error = err;\n next();\n });\n\n await withServer(app, async ({ url }) => {\n const res = await fetch(url);\n\n assert.equal(res.status, 200);\n assert.equal(error?.message, 'oops');\n });\n });\n});\n"]}
|
package/dist/cookie.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { Request } from 'express';
|
|
2
2
|
export type CookieSecure = boolean | 'auto' | ((req: Request) => boolean);
|
|
3
3
|
export declare function shouldSecureCookie(req: Request, secure: CookieSecure): boolean;
|
|
4
|
-
export declare function getSessionCookie(req: Request, cookieNames: string[]): {
|
|
5
|
-
name: string | null;
|
|
6
|
-
value: string;
|
|
7
|
-
} | null;
|
|
8
4
|
export declare function getSessionIdFromCookie(sessionCookie: string | null | undefined, secrets: string[]): string | null;
|
package/dist/cookie.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getSessionIdFromCookie = exports.getSessionCookie = 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) {
|
|
1
|
+
import signature from 'cookie-signature';
|
|
2
|
+
export function shouldSecureCookie(req, secure) {
|
|
10
3
|
if (typeof secure === 'function') {
|
|
11
4
|
return secure(req);
|
|
12
5
|
}
|
|
@@ -15,32 +8,11 @@ function shouldSecureCookie(req, secure) {
|
|
|
15
8
|
}
|
|
16
9
|
return secure;
|
|
17
10
|
}
|
|
18
|
-
|
|
19
|
-
function getSessionCookie(req, cookieNames) {
|
|
20
|
-
const cookies = cookie_1.default.parse(req.headers.cookie ?? '');
|
|
21
|
-
// Try each cookie name until we find one that's present.
|
|
22
|
-
let foundCookieName = null;
|
|
23
|
-
let sessionCookie = null;
|
|
24
|
-
for (const cookieName of cookieNames) {
|
|
25
|
-
if (cookies[cookieName]) {
|
|
26
|
-
foundCookieName = cookieName;
|
|
27
|
-
sessionCookie = cookies[cookieName];
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (!sessionCookie)
|
|
32
|
-
return null;
|
|
33
|
-
return {
|
|
34
|
-
name: foundCookieName,
|
|
35
|
-
value: sessionCookie,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
exports.getSessionCookie = getSessionCookie;
|
|
39
|
-
function getSessionIdFromCookie(sessionCookie, secrets) {
|
|
11
|
+
export function getSessionIdFromCookie(sessionCookie, secrets) {
|
|
40
12
|
// Try each secret until we find one that works.
|
|
41
13
|
if (sessionCookie) {
|
|
42
14
|
for (const secret of secrets) {
|
|
43
|
-
const value =
|
|
15
|
+
const value = signature.unsign(sessionCookie, secret);
|
|
44
16
|
if (value !== false) {
|
|
45
17
|
return value;
|
|
46
18
|
}
|
|
@@ -48,5 +20,4 @@ function getSessionIdFromCookie(sessionCookie, secrets) {
|
|
|
48
20
|
}
|
|
49
21
|
return null;
|
|
50
22
|
}
|
|
51
|
-
exports.getSessionIdFromCookie = getSessionIdFromCookie;
|
|
52
23
|
//# sourceMappingURL=cookie.js.map
|
package/dist/cookie.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookie.js","sourceRoot":"","sources":["../src/cookie.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cookie.js","sourceRoot":"","sources":["../src/cookie.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,kBAAkB,CAAC;AAKzC,MAAM,UAAU,kBAAkB,CAAC,GAAY,EAAE,MAAoB;IACnE,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAED,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC;IAClC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,aAAwC,EACxC,OAAiB;IAEjB,gDAAgD;IAChD,IAAI,aAAa,EAAE,CAAC;QAClB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;YACtD,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;gBACpB,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import signature from 'cookie-signature';\nimport type { Request } from 'express';\n\nexport type CookieSecure = boolean | 'auto' | ((req: Request) => boolean);\n\nexport function shouldSecureCookie(req: Request, secure: CookieSecure): boolean {\n if (typeof secure === 'function') {\n return secure(req);\n }\n\n if (secure === 'auto') {\n return req.protocol === 'https';\n }\n\n return secure;\n}\n\nexport function getSessionIdFromCookie(\n sessionCookie: string | null | undefined,\n secrets: string[],\n) {\n // Try each secret until we find one that works.\n if (sessionCookie) {\n for (const secret of secrets) {\n const value = signature.unsign(sessionCookie, secret);\n if (value !== false) {\n return value;\n }\n }\n }\n\n return null;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SessionStore } from './store';
|
|
2
|
-
import { type CookieSecure } from './cookie';
|
|
3
|
-
import { type Session } from './session';
|
|
1
|
+
import { type SessionStore } from './store.js';
|
|
2
|
+
import { type CookieSecure } from './cookie.js';
|
|
3
|
+
import { type Session } from './session.js';
|
|
4
4
|
declare global {
|
|
5
5
|
namespace Express {
|
|
6
6
|
interface Request {
|
|
@@ -8,22 +8,33 @@ declare global {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
interface CookieOptions {
|
|
12
|
+
secure?: CookieSecure;
|
|
13
|
+
httpOnly?: boolean;
|
|
14
|
+
domain?: string;
|
|
15
|
+
sameSite?: boolean | 'none' | 'lax' | 'strict';
|
|
16
|
+
maxAge?: number;
|
|
17
|
+
}
|
|
11
18
|
export interface SessionOptions {
|
|
12
19
|
secret: string | string[];
|
|
13
20
|
store: SessionStore;
|
|
14
|
-
cookie?: {
|
|
21
|
+
cookie?: CookieOptions & {
|
|
22
|
+
/**
|
|
23
|
+
* The name of the session cookie. The session is always read from this
|
|
24
|
+
* named cookie, but it may be written to multiple cookies if `writeNames`
|
|
25
|
+
* is provided.
|
|
26
|
+
*/
|
|
27
|
+
name?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Multiple write names can be provided to allow for a session cookie to be
|
|
30
|
+
* written to multiple names. This can be useful for a migration of a cookie
|
|
31
|
+
* to an explicit subdomain, for example.
|
|
32
|
+
*/
|
|
33
|
+
writeNames?: string[];
|
|
15
34
|
/**
|
|
16
|
-
*
|
|
17
|
-
* name for setting the cookie. The other names are used as fallbacks when
|
|
18
|
-
* reading the cookie in requests. If a fallback name is found in a request,
|
|
19
|
-
* the cookie value will be transparently re-written to the primary name.
|
|
35
|
+
* Used with `writeNames` to provide additional options for each written cookie.
|
|
20
36
|
*/
|
|
21
|
-
|
|
22
|
-
secure?: CookieSecure;
|
|
23
|
-
httpOnly?: boolean;
|
|
24
|
-
domain?: string;
|
|
25
|
-
sameSite?: boolean | 'none' | 'lax' | 'strict';
|
|
26
|
-
maxAge?: number;
|
|
37
|
+
writeOverrides?: Omit<CookieOptions, 'secure'>[];
|
|
27
38
|
};
|
|
28
39
|
}
|
|
29
40
|
export { SessionStore };
|
package/dist/index.js
CHANGED
|
@@ -1,62 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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");
|
|
1
|
+
import onHeaders from 'on-headers';
|
|
2
|
+
import signature from 'cookie-signature';
|
|
3
|
+
import asyncHandler from 'express-async-handler';
|
|
4
|
+
import cookie from 'cookie';
|
|
5
|
+
import { beforeEnd } from './before-end.js';
|
|
6
|
+
import { shouldSecureCookie, getSessionIdFromCookie } from './cookie.js';
|
|
7
|
+
import { generateSessionId, loadSession, hashSession, truncateExpirationDate, } from './session.js';
|
|
13
8
|
const DEFAULT_COOKIE_NAME = 'session';
|
|
14
9
|
const DEFAULT_COOKIE_MAX_AGE = 86400000; // 1 day
|
|
15
|
-
function createSessionMiddleware(options) {
|
|
10
|
+
export function createSessionMiddleware(options) {
|
|
16
11
|
const secrets = Array.isArray(options.secret) ? options.secret : [options.secret];
|
|
17
|
-
const
|
|
18
|
-
const primaryCookieName = cookieNames[0];
|
|
12
|
+
const cookieName = options.cookie?.name ?? DEFAULT_COOKIE_NAME;
|
|
19
13
|
const cookieMaxAge = options.cookie?.maxAge ?? DEFAULT_COOKIE_MAX_AGE;
|
|
20
14
|
const store = options.store;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
// Ensure that the session cookie that we're reading from will be written to.
|
|
16
|
+
const writeCookieNames = options.cookie?.writeNames ?? [cookieName];
|
|
17
|
+
if (!writeCookieNames.includes(cookieName)) {
|
|
18
|
+
throw new Error('cookie.name must be included in cookie.writeNames');
|
|
19
|
+
}
|
|
20
|
+
// Validate write overrides.
|
|
21
|
+
if (options.cookie?.writeOverrides && !options.cookie.writeNames) {
|
|
22
|
+
throw new Error('cookie.writeOverrides must be used with cookie.writeNames');
|
|
23
|
+
}
|
|
24
|
+
if (options.cookie?.writeOverrides &&
|
|
25
|
+
options.cookie.writeOverrides.length !== writeCookieNames.length) {
|
|
26
|
+
throw new Error('cookie.writeOverrides must have the same length as cookie.writeNames');
|
|
27
|
+
}
|
|
28
|
+
return asyncHandler(async function sessionMiddleware(req, res, next) {
|
|
29
|
+
const cookies = cookie.parse(req.headers.cookie ?? '');
|
|
30
|
+
const sessionCookie = cookies[cookieName];
|
|
31
|
+
const cookieSessionId = getSessionIdFromCookie(sessionCookie, secrets);
|
|
32
|
+
const sessionId = cookieSessionId ?? (await generateSessionId());
|
|
33
|
+
req.session = await loadSession(sessionId, req, store, cookieMaxAge);
|
|
34
|
+
const originalHash = hashSession(req.session);
|
|
27
35
|
const originalExpirationDate = req.session.getExpirationDate();
|
|
28
|
-
(
|
|
36
|
+
onHeaders(res, () => {
|
|
29
37
|
if (!req.session) {
|
|
30
38
|
if (cookieSessionId) {
|
|
31
39
|
// If the request arrived with a session cookie but the session was
|
|
32
40
|
// destroyed, clear the cookie.
|
|
33
41
|
//
|
|
34
42
|
// To cover all our bases, we'll clear *all* known session cookies to
|
|
35
|
-
// ensure that state sessions aren't left behind.
|
|
36
|
-
|
|
43
|
+
// ensure that state sessions aren't left behind. We'll also send commands
|
|
44
|
+
// to clear the cookies both on and off the explicit domain, to handle
|
|
45
|
+
// the case where the application has moved from one domain to another.
|
|
46
|
+
writeCookieNames.forEach((cookieName, i) => {
|
|
37
47
|
res.clearCookie(cookieName);
|
|
48
|
+
const domain = options.cookie?.writeOverrides?.[i]?.domain ?? options.cookie?.domain;
|
|
49
|
+
if (domain) {
|
|
50
|
+
res.clearCookie(cookieName, { domain: options.cookie?.domain });
|
|
51
|
+
}
|
|
38
52
|
});
|
|
39
53
|
return;
|
|
40
54
|
}
|
|
41
55
|
// There is no session to do anything with.
|
|
42
56
|
return;
|
|
43
57
|
}
|
|
44
|
-
const secureCookie =
|
|
58
|
+
const secureCookie = shouldSecureCookie(req, options.cookie?.secure ?? 'auto');
|
|
45
59
|
if (secureCookie && req.protocol !== 'https') {
|
|
46
60
|
// Avoid sending cookie over insecure connection.
|
|
47
61
|
return;
|
|
48
62
|
}
|
|
49
|
-
|
|
63
|
+
// Ensure that all known session cookies are set to the same value.
|
|
64
|
+
const hasAllCookies = writeCookieNames.every((cookieName) => !!cookies[cookieName]);
|
|
50
65
|
const isNewSession = !cookieSessionId || cookieSessionId !== req.session.id;
|
|
51
66
|
const didExpirationChange = originalExpirationDate.getTime() !== req.session.getExpirationDate().getTime();
|
|
52
|
-
if (isNewSession || didExpirationChange ||
|
|
67
|
+
if (isNewSession || didExpirationChange || !hasAllCookies) {
|
|
53
68
|
const signedSessionId = signSessionId(req.session.id, secrets[0]);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
writeCookieNames.forEach((cookieName, i) => {
|
|
70
|
+
res.cookie(cookieName, signedSessionId, {
|
|
71
|
+
secure: secureCookie,
|
|
72
|
+
httpOnly: options.cookie?.httpOnly ?? true,
|
|
73
|
+
domain: options.cookie?.domain,
|
|
74
|
+
sameSite: options.cookie?.sameSite ?? false,
|
|
75
|
+
expires: req.session.getExpirationDate(),
|
|
76
|
+
...(options.cookie?.writeOverrides?.[i] ?? {}),
|
|
77
|
+
});
|
|
60
78
|
});
|
|
61
79
|
}
|
|
62
80
|
});
|
|
@@ -74,19 +92,19 @@ function createSessionMiddleware(options) {
|
|
|
74
92
|
// the updated data to the store. However, if the hash didn't change, we
|
|
75
93
|
// only want to persist it if the expiration changed *and* if we can set
|
|
76
94
|
// a cookie to reflect the updated expiration date.
|
|
77
|
-
const hashChanged =
|
|
95
|
+
const hashChanged = hashSession(req.session) !== originalHash;
|
|
78
96
|
const expirationChanged = originalExpirationDate.getTime() !== req.session.getExpirationDate().getTime();
|
|
79
97
|
if (hashChanged || expirationChanged) {
|
|
80
98
|
await store.set(req.session.id, req.session,
|
|
81
99
|
// Cookies only support second-level resolution. To ensure consistency
|
|
82
100
|
// between the cookie and the store, truncate the expiration date to
|
|
83
101
|
// the nearest second.
|
|
84
|
-
|
|
102
|
+
truncateExpirationDate(req.session.getExpirationDate()));
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
105
|
// We'll attempt to persist the session at the end of the request. This
|
|
88
106
|
// hacky strategy is borrowed from `express-session`.
|
|
89
|
-
|
|
107
|
+
beforeEnd(res, next, async () => {
|
|
90
108
|
await persistSession(req);
|
|
91
109
|
});
|
|
92
110
|
// We'll also attempt to persist the session before performing a redirect.
|
|
@@ -102,17 +120,7 @@ function createSessionMiddleware(options) {
|
|
|
102
120
|
next();
|
|
103
121
|
});
|
|
104
122
|
}
|
|
105
|
-
exports.createSessionMiddleware = createSessionMiddleware;
|
|
106
|
-
function getCookieNames(cookieName) {
|
|
107
|
-
if (!cookieName) {
|
|
108
|
-
return [DEFAULT_COOKIE_NAME];
|
|
109
|
-
}
|
|
110
|
-
if (Array.isArray(cookieName) && cookieName.length === 0) {
|
|
111
|
-
throw new Error('cookie.name must not be an empty array');
|
|
112
|
-
}
|
|
113
|
-
return Array.isArray(cookieName) ? cookieName : [cookieName];
|
|
114
|
-
}
|
|
115
123
|
function signSessionId(sessionId, secret) {
|
|
116
|
-
return
|
|
124
|
+
return signature.sign(sessionId, secret);
|
|
117
125
|
}
|
|
118
126
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Request, Response, NextFunction } from 'express';\nimport onHeaders from 'on-headers';\nimport signature from 'cookie-signature';\nimport asyncHandler from 'express-async-handler';\nimport cookie from 'cookie';\n\nimport { type SessionStore } from './store.js';\nimport { beforeEnd } from './before-end.js';\nimport { type CookieSecure, shouldSecureCookie, getSessionIdFromCookie } from './cookie.js';\nimport {\n type Session,\n generateSessionId,\n loadSession,\n hashSession,\n truncateExpirationDate,\n} from './session.js';\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n session: Session;\n }\n }\n}\n\ninterface CookieOptions {\n secure?: CookieSecure;\n httpOnly?: boolean;\n domain?: string;\n sameSite?: boolean | 'none' | 'lax' | 'strict';\n maxAge?: number;\n}\n\nexport interface SessionOptions {\n secret: string | string[];\n store: SessionStore;\n cookie?: CookieOptions & {\n /**\n * The name of the session cookie. The session is always read from this\n * named cookie, but it may be written to multiple cookies if `writeNames`\n * is provided.\n */\n name?: string;\n /**\n * Multiple write names can be provided to allow for a session cookie to be\n * written to multiple names. This can be useful for a migration of a cookie\n * to an explicit subdomain, for example.\n */\n writeNames?: string[];\n /**\n * Used with `writeNames` to provide additional options for each written cookie.\n */\n writeOverrides?: Omit<CookieOptions, 'secure'>[];\n };\n}\n\nexport { SessionStore };\n\nconst DEFAULT_COOKIE_NAME = 'session';\nconst DEFAULT_COOKIE_MAX_AGE = 86400000; // 1 day\n\nexport function createSessionMiddleware(options: SessionOptions) {\n const secrets = Array.isArray(options.secret) ? options.secret : [options.secret];\n const cookieName = options.cookie?.name ?? DEFAULT_COOKIE_NAME;\n const cookieMaxAge = options.cookie?.maxAge ?? DEFAULT_COOKIE_MAX_AGE;\n const store = options.store;\n\n // Ensure that the session cookie that we're reading from will be written to.\n const writeCookieNames = options.cookie?.writeNames ?? [cookieName];\n if (!writeCookieNames.includes(cookieName)) {\n throw new Error('cookie.name must be included in cookie.writeNames');\n }\n\n // Validate write overrides.\n if (options.cookie?.writeOverrides && !options.cookie.writeNames) {\n throw new Error('cookie.writeOverrides must be used with cookie.writeNames');\n }\n if (\n options.cookie?.writeOverrides &&\n options.cookie.writeOverrides.length !== writeCookieNames.length\n ) {\n throw new Error('cookie.writeOverrides must have the same length as cookie.writeNames');\n }\n\n return asyncHandler(async function sessionMiddleware(\n req: Request,\n res: Response,\n next: NextFunction,\n ) {\n const cookies = cookie.parse(req.headers.cookie ?? '');\n const sessionCookie = cookies[cookieName];\n const cookieSessionId = getSessionIdFromCookie(sessionCookie, secrets);\n const sessionId = cookieSessionId ?? (await generateSessionId());\n req.session = await loadSession(sessionId, req, store, cookieMaxAge);\n\n const originalHash = hashSession(req.session);\n const originalExpirationDate = req.session.getExpirationDate();\n\n onHeaders(res, () => {\n if (!req.session) {\n if (cookieSessionId) {\n // If the request arrived with a session cookie but the session was\n // destroyed, clear the cookie.\n //\n // To cover all our bases, we'll clear *all* known session cookies to\n // ensure that state sessions aren't left behind. We'll also send commands\n // to clear the cookies both on and off the explicit domain, to handle\n // the case where the application has moved from one domain to another.\n writeCookieNames.forEach((cookieName, i) => {\n res.clearCookie(cookieName);\n const domain = options.cookie?.writeOverrides?.[i]?.domain ?? options.cookie?.domain;\n if (domain) {\n res.clearCookie(cookieName, { domain: options.cookie?.domain });\n }\n });\n return;\n }\n\n // There is no session to do anything with.\n return;\n }\n\n const secureCookie = shouldSecureCookie(req, options.cookie?.secure ?? 'auto');\n if (secureCookie && req.protocol !== 'https') {\n // Avoid sending cookie over insecure connection.\n return;\n }\n\n // Ensure that all known session cookies are set to the same value.\n const hasAllCookies = writeCookieNames.every((cookieName) => !!cookies[cookieName]);\n const isNewSession = !cookieSessionId || cookieSessionId !== req.session.id;\n const didExpirationChange =\n originalExpirationDate.getTime() !== req.session.getExpirationDate().getTime();\n if (isNewSession || didExpirationChange || !hasAllCookies) {\n const signedSessionId = signSessionId(req.session.id, secrets[0]);\n writeCookieNames.forEach((cookieName, i) => {\n res.cookie(cookieName, signedSessionId, {\n secure: secureCookie,\n httpOnly: options.cookie?.httpOnly ?? true,\n domain: options.cookie?.domain,\n sameSite: options.cookie?.sameSite ?? false,\n expires: req.session.getExpirationDate(),\n ...(options.cookie?.writeOverrides?.[i] ?? {}),\n });\n });\n }\n });\n\n let sessionPersisted = false;\n\n async function persistSession(req: Request) {\n if (!req.session || sessionPersisted) {\n // There is no session to do anything with.\n return;\n }\n\n sessionPersisted = true;\n\n // If this is a new session, we would have already persisted it to the\n // store, so we don't need to take that into consideration here.\n //\n // If the hash of the session data changed, we'll unconditionally persist\n // the updated data to the store. However, if the hash didn't change, we\n // only want to persist it if the expiration changed *and* if we can set\n // a cookie to reflect the updated expiration date.\n const hashChanged = hashSession(req.session) !== originalHash;\n const expirationChanged =\n originalExpirationDate.getTime() !== req.session.getExpirationDate().getTime();\n if (hashChanged || expirationChanged) {\n await store.set(\n req.session.id,\n req.session,\n // Cookies only support second-level resolution. To ensure consistency\n // between the cookie and the store, truncate the expiration date to\n // the nearest second.\n truncateExpirationDate(req.session.getExpirationDate()),\n );\n }\n }\n\n // We'll attempt to persist the session at the end of the request. This\n // hacky strategy is borrowed from `express-session`.\n beforeEnd(res, next, async () => {\n await persistSession(req);\n });\n\n // We'll also attempt to persist the session before performing a redirect.\n // This is necessary because browsers and `fetch()` implementations aren't\n // required to wait for a response body to be received before following a\n // redirect. So, we need to make sure that the session is persisted before\n // we send the redirect response. This way, the subsequent GET will be able\n // to load the latest session data.\n const originalRedirect = res.redirect as any;\n res.redirect = function redirect(...args: any[]) {\n persistSession(req).then(\n () => originalRedirect.apply(res, args),\n (err) => next(err),\n );\n };\n\n next();\n });\n}\n\nfunction signSessionId(sessionId: string, secret: string): string {\n return signature.sign(sessionId, secret);\n}\n"]}
|