@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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +0 -0
  2. package/README.md +39 -0
  3. package/dist/before-end.d.ts +25 -0
  4. package/dist/before-end.js +81 -0
  5. package/dist/before-end.js.map +1 -0
  6. package/dist/before-end.test.d.ts +1 -0
  7. package/dist/before-end.test.js +33 -0
  8. package/dist/before-end.test.js.map +1 -0
  9. package/dist/cookie.d.ts +4 -0
  10. package/dist/cookie.js +34 -0
  11. package/dist/cookie.js.map +1 -0
  12. package/dist/index.d.ts +24 -0
  13. package/dist/index.js +89 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/index.test.d.ts +1 -0
  16. package/dist/index.test.js +506 -0
  17. package/dist/index.test.js.map +1 -0
  18. package/dist/memory-store.d.ts +7 -0
  19. package/dist/memory-store.js +28 -0
  20. package/dist/memory-store.js.map +1 -0
  21. package/dist/session.d.ts +14 -0
  22. package/dist/session.js +78 -0
  23. package/dist/session.js.map +1 -0
  24. package/dist/session.test.d.ts +1 -0
  25. package/dist/session.test.js +92 -0
  26. package/dist/session.test.js.map +1 -0
  27. package/dist/store.d.ts +9 -0
  28. package/dist/store.js +3 -0
  29. package/dist/store.js.map +1 -0
  30. package/dist/test-utils.d.ts +10 -0
  31. package/dist/test-utils.js +32 -0
  32. package/dist/test-utils.js.map +1 -0
  33. package/package.json +44 -0
  34. package/src/before-end.test.ts +34 -0
  35. package/src/before-end.ts +96 -0
  36. package/src/cookie.ts +38 -0
  37. package/src/index.test.ts +628 -0
  38. package/src/index.ts +132 -0
  39. package/src/memory-store.ts +25 -0
  40. package/src/session.test.ts +122 -0
  41. package/src/session.ts +106 -0
  42. package/src/store.ts +10 -0
  43. package/src/test-utils.ts +42 -0
  44. package/tsconfig.json +11 -0
@@ -0,0 +1,78 @@
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.hashSession = exports.makeSession = exports.loadSession = exports.generateSessionId = void 0;
7
+ const uid_safe_1 = __importDefault(require("uid-safe"));
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ async function generateSessionId() {
10
+ return await (0, uid_safe_1.default)(24);
11
+ }
12
+ exports.generateSessionId = generateSessionId;
13
+ async function loadSession(sessionId, req, store, maxAge) {
14
+ const sessionStoreData = await store.get(sessionId);
15
+ const expiresAt = sessionStoreData?.expiresAt ?? null;
16
+ const session = makeSession(sessionId, req, store, expiresAt, maxAge);
17
+ // Copy session data into the session object.
18
+ if (sessionStoreData != null) {
19
+ const { data } = sessionStoreData;
20
+ for (const prop in data) {
21
+ if (!(prop in session)) {
22
+ session[prop] = data[prop];
23
+ }
24
+ }
25
+ }
26
+ return session;
27
+ }
28
+ exports.loadSession = loadSession;
29
+ function makeSession(sessionId, req, store, expirationDate, maxAge) {
30
+ const session = {};
31
+ let expiresAt = expirationDate;
32
+ defineStaticProperty(session, 'id', sessionId);
33
+ defineStaticProperty(session, 'destroy', async () => {
34
+ delete req.session;
35
+ await store.destroy(sessionId);
36
+ });
37
+ defineStaticProperty(session, 'regenerate', async () => {
38
+ await store.destroy(sessionId);
39
+ req.session = makeSession(await generateSessionId(), req, store, null, maxAge);
40
+ });
41
+ defineStaticProperty(session, 'getExpirationDate', () => {
42
+ if (expiresAt == null) {
43
+ expiresAt = new Date(Date.now() + maxAge);
44
+ }
45
+ return expiresAt;
46
+ });
47
+ defineStaticProperty(session, 'setExpiration', (expiration) => {
48
+ if (typeof expiration === 'number') {
49
+ expiresAt = new Date(Date.now() + expiration);
50
+ }
51
+ else {
52
+ expiresAt = expiration;
53
+ }
54
+ });
55
+ return session;
56
+ }
57
+ exports.makeSession = makeSession;
58
+ function hashSession(session) {
59
+ const str = JSON.stringify(session, function (key, val) {
60
+ // ignore cookie property on the root object
61
+ if (this === session && key === 'cookie') {
62
+ return;
63
+ }
64
+ return val;
65
+ });
66
+ // hash
67
+ return node_crypto_1.default.createHash('sha1').update(str, 'utf8').digest('hex');
68
+ }
69
+ exports.hashSession = hashSession;
70
+ function defineStaticProperty(obj, name, fn) {
71
+ Object.defineProperty(obj, name, {
72
+ configurable: false,
73
+ enumerable: false,
74
+ writable: false,
75
+ value: fn,
76
+ });
77
+ }
78
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const memory_store_1 = require("./memory-store");
5
+ const session_1 = require("./session");
6
+ const SESSION_MAX_AGE = 10000;
7
+ const SESSION_EXPIRATION_DATE = new Date(Date.now() + SESSION_MAX_AGE);
8
+ describe('session', () => {
9
+ describe('loadSession', () => {
10
+ it('loads session that does not exist', async () => {
11
+ const store = new memory_store_1.MemoryStore();
12
+ const req = {};
13
+ const session = await (0, session_1.loadSession)('123', req, store, SESSION_MAX_AGE);
14
+ chai_1.assert.equal(session.id, '123');
15
+ });
16
+ it('loads session from store', async () => {
17
+ const store = new memory_store_1.MemoryStore();
18
+ await store.set('123', { foo: 'bar' }, SESSION_EXPIRATION_DATE);
19
+ const req = {};
20
+ const session = await (0, session_1.loadSession)('123', req, store, SESSION_MAX_AGE);
21
+ chai_1.assert.equal(session.id, '123');
22
+ chai_1.assert.equal(session.foo, 'bar');
23
+ });
24
+ it('does not try to overwrite existing session properties', async () => {
25
+ const store = new memory_store_1.MemoryStore();
26
+ await store.set('123', { foo: 'bar', id: '456' }, SESSION_EXPIRATION_DATE);
27
+ const req = {};
28
+ const session = await (0, session_1.loadSession)('123', req, store, SESSION_MAX_AGE);
29
+ chai_1.assert.equal(session.id, '123');
30
+ chai_1.assert.equal(session.foo, 'bar');
31
+ });
32
+ });
33
+ describe('makeSession', () => {
34
+ it('has immutable properties', () => {
35
+ const store = new memory_store_1.MemoryStore();
36
+ const req = {};
37
+ const session = (0, session_1.makeSession)('123', req, store, SESSION_EXPIRATION_DATE, SESSION_MAX_AGE);
38
+ chai_1.assert.equal(session.id, '123');
39
+ const originalId = session.id;
40
+ const originalDestroy = session.destroy;
41
+ const originalRegenerate = session.regenerate;
42
+ chai_1.assert.throw(() => {
43
+ session.id = '456';
44
+ });
45
+ chai_1.assert.throw(() => {
46
+ session.destroy = async () => { };
47
+ });
48
+ chai_1.assert.throw(() => {
49
+ session.regenerate = async () => { };
50
+ });
51
+ chai_1.assert.equal(session.id, originalId);
52
+ chai_1.assert.equal(session.destroy, originalDestroy);
53
+ chai_1.assert.equal(session.regenerate, originalRegenerate);
54
+ });
55
+ it('has immutable destroy property', async () => {
56
+ const store = new memory_store_1.MemoryStore();
57
+ const req = {};
58
+ const session = (0, session_1.makeSession)('123', req, store, SESSION_EXPIRATION_DATE, SESSION_MAX_AGE);
59
+ chai_1.assert.throw(() => {
60
+ session.destroy = async () => { };
61
+ });
62
+ });
63
+ it('can destroy itself', async () => {
64
+ const store = new memory_store_1.MemoryStore();
65
+ const req = {};
66
+ const session = (0, session_1.makeSession)('123', req, store, SESSION_EXPIRATION_DATE, SESSION_MAX_AGE);
67
+ req.session = session;
68
+ await session.destroy();
69
+ chai_1.assert.isUndefined(req.session);
70
+ chai_1.assert.isNull(await store.get('123'));
71
+ });
72
+ it('can regenerate itself', async () => {
73
+ const store = new memory_store_1.MemoryStore();
74
+ const req = {};
75
+ const session = (0, session_1.makeSession)('123', req, store, SESSION_EXPIRATION_DATE, SESSION_MAX_AGE);
76
+ req.session = session;
77
+ await session.regenerate();
78
+ chai_1.assert.notEqual(req.session, session);
79
+ chai_1.assert.notEqual(req.session.id, '123');
80
+ chai_1.assert.isNull(await store.get('123'));
81
+ });
82
+ });
83
+ describe('hashSession', () => {
84
+ it('ignores the cookie property', () => {
85
+ const hash1 = (0, session_1.hashSession)({ id: '123' });
86
+ const hash2 = (0, session_1.hashSession)({ id: '123', cookie: { foo: 'bar' } });
87
+ chai_1.assert.equal(hash1, hash2);
88
+ });
89
+ });
90
+ it('has cookie property', () => { });
91
+ });
92
+ //# sourceMappingURL=session.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.test.js","sourceRoot":"","sources":["../src/session.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export interface SessionStoreData {
2
+ data: any;
3
+ expiresAt: Date;
4
+ }
5
+ export interface SessionStore {
6
+ set(id: string, session: any, expiresAt: Date): Promise<void>;
7
+ get(id: string): Promise<SessionStoreData | null>;
8
+ destroy(id: string): Promise<void>;
9
+ }
package/dist/store.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ /// <reference types="node" />
2
+ import express from 'express';
3
+ import { Server } from 'node:http';
4
+ interface WithServerContext {
5
+ server: Server;
6
+ port: number;
7
+ url: string;
8
+ }
9
+ export declare function withServer(app: express.Express, fn: (ctx: WithServerContext) => Promise<void>): Promise<void>;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withServer = void 0;
4
+ async function withServer(app, fn) {
5
+ const server = app.listen();
6
+ await new Promise((resolve, reject) => {
7
+ server.on('listening', () => resolve());
8
+ server.on('error', (err) => reject(err));
9
+ });
10
+ try {
11
+ await fn({
12
+ server,
13
+ port: getServerPort(server),
14
+ url: `http://localhost:${getServerPort(server)}`,
15
+ });
16
+ }
17
+ finally {
18
+ server.close();
19
+ }
20
+ }
21
+ exports.withServer = withServer;
22
+ function getServerPort(server) {
23
+ const address = server.address();
24
+ // istanbul ignore next
25
+ if (!address)
26
+ throw new Error('Server is not listening');
27
+ // istanbul ignore next
28
+ if (typeof address === 'string')
29
+ throw new Error('Server is listening on a pipe');
30
+ return address.port;
31
+ }
32
+ //# sourceMappingURL=test-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-utils.js","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":";;;AASO,KAAK,UAAU,UAAU,CAC9B,GAAoB,EACpB,EAA6C;IAE7C,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;IAE5B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QACxC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,IAAI;QACF,MAAM,EAAE,CAAC;YACP,MAAM;YACN,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC;YAC3B,GAAG,EAAE,oBAAoB,aAAa,CAAC,MAAM,CAAC,EAAE;SACjD,CAAC,CAAC;KACJ;YAAS;QACR,MAAM,CAAC,KAAK,EAAE,CAAC;KAChB;AACH,CAAC;AApBD,gCAoBC;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IAEjC,uBAAuB;IACvB,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAEzD,uBAAuB;IACvB,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAElF,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@prairielearn/session",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/PrairieLearn/PrairieLearn.git",
8
+ "directory": "packages/session"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch --preserveWatchOutput",
13
+ "test": "nyc --reporter=html --reporter=text mocha --no-config --require ts-node/register --watch-files src/ src/**/*.test.ts"
14
+ },
15
+ "dependencies": {
16
+ "cookie": "^0.5.0",
17
+ "cookie-signature": "^1.2.1",
18
+ "express-async-handler": "^1.2.0",
19
+ "nyc": "^15.1.0",
20
+ "on-headers": "^1.0.2",
21
+ "uid-safe": "^2.1.5"
22
+ },
23
+ "devDependencies": {
24
+ "@prairielearn/tsconfig": "^0.0.0",
25
+ "@types/chai": "^4.3.5",
26
+ "@types/cookie": "^0.5.1",
27
+ "@types/cookie-signature": "^1.1.0",
28
+ "@types/express": "^4.17.17",
29
+ "@types/node": "^18.17.12",
30
+ "@types/node-fetch": "^2.6.4",
31
+ "@types/on-headers": "^1.0.0",
32
+ "@types/set-cookie-parser": "^2.4.3",
33
+ "@types/uid-safe": "^2.1.2",
34
+ "chai": "^4.3.8",
35
+ "express": "^4.18.2",
36
+ "fetch-cookie": "^2.1.0",
37
+ "mocha": "^10.2.0",
38
+ "node-fetch": "^2.7.0",
39
+ "set-cookie-parser": "^2.6.0",
40
+ "source-map-support": "^0.5.21",
41
+ "ts-node": "^10.9.1",
42
+ "typescript": "^5.2.2"
43
+ }
44
+ }
@@ -0,0 +1,34 @@
1
+ import express, { type Request, type Response, type NextFunction } from 'express';
2
+ import fetch from 'node-fetch';
3
+ import { assert } from 'chai';
4
+
5
+ import { beforeEnd } from './before-end';
6
+ import { withServer } from './test-utils';
7
+
8
+ describe('beforeEnd', () => {
9
+ it('handles errors correctly', async () => {
10
+ const app = express();
11
+ app.use((_req, res, next) => {
12
+ beforeEnd(res, next, async () => {
13
+ throw new Error('oops');
14
+ });
15
+
16
+ next();
17
+ });
18
+
19
+ app.get('/', (_req, res) => res.sendStatus(200));
20
+
21
+ let error: Error | null = null;
22
+ app.use((err: any, _req: Request, _res: Response, next: NextFunction) => {
23
+ error = err;
24
+ next();
25
+ });
26
+
27
+ await withServer(app, async ({ url }) => {
28
+ const res = await fetch(url);
29
+
30
+ assert.equal(res.status, 200);
31
+ assert.equal(error?.message, 'oops');
32
+ });
33
+ });
34
+ });
@@ -0,0 +1,96 @@
1
+ import type { NextFunction } from 'express';
2
+
3
+ /**
4
+ * The following function is based on code from `express-session`:
5
+ *
6
+ * https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L246-L360
7
+ *
8
+ * This code is used to work around the fact that Express doesn't have a good
9
+ * hook to allow us to perform some asynchronous operation before the response
10
+ * is written to the client.
11
+ *
12
+ * Note that this is truly only necessary for Express. Other Node frameworks
13
+ * like Fastify and Adonis have hooks that allow us to do this without any
14
+ * hacks. It's also probably only useful in the context of Express, as it
15
+ * seems to rely on the fact that Express and its ecosystem generally don't
16
+ * call `end()` without an additional chunk of data. If it instead called
17
+ * `write()` with the final data and then `end()` with no data, this code
18
+ * wouldn't function as intended. It's possible that `stream.pipe(res)` does
19
+ * in fact behave this way, so it's probably not completely safe to use this
20
+ * code when streaming responses back to the client.
21
+ *
22
+ * One could probably make this safer by *also* hooking into `response.write()`
23
+ * and buffering the data. My understanding of Node streams isn't good enough
24
+ * to implement that, though.
25
+ */
26
+ export function beforeEnd(res: any, next: NextFunction, fn: () => Promise<void>) {
27
+ const _end = res.end as any;
28
+ const _write = res.write as any;
29
+ let ended = false;
30
+
31
+ res.end = function end(chunk: any, encoding: any) {
32
+ if (ended) {
33
+ return false;
34
+ }
35
+
36
+ ended = true;
37
+
38
+ let ret: any;
39
+ let sync = true;
40
+
41
+ function writeend() {
42
+ if (sync) {
43
+ ret = _end.call(res, chunk, encoding);
44
+ sync = false;
45
+ return;
46
+ }
47
+
48
+ _end.call(res);
49
+ }
50
+
51
+ function writetop() {
52
+ if (!sync) {
53
+ return ret;
54
+ }
55
+
56
+ if (!res._header) {
57
+ res._implicitHeader();
58
+ }
59
+
60
+ if (chunk == null) {
61
+ ret = true;
62
+ return ret;
63
+ }
64
+
65
+ const contentLength = Number(res.getHeader('Content-Length'));
66
+
67
+ if (!isNaN(contentLength) && contentLength > 0) {
68
+ chunk = !Buffer.isBuffer(chunk) ? Buffer.from(chunk, encoding) : chunk;
69
+ encoding = undefined;
70
+
71
+ if (chunk.length !== 0) {
72
+ ret = _write.call(res, chunk.slice(0, chunk.length - 1));
73
+ chunk = chunk.slice(chunk.length - 1, chunk.length);
74
+ return ret;
75
+ }
76
+ }
77
+
78
+ ret = _write.call(res, chunk, encoding);
79
+ sync = false;
80
+
81
+ return ret;
82
+ }
83
+
84
+ fn().then(
85
+ () => {
86
+ writeend();
87
+ },
88
+ (err) => {
89
+ setImmediate(next, err);
90
+ writeend();
91
+ },
92
+ );
93
+
94
+ return writetop();
95
+ };
96
+ }
package/src/cookie.ts ADDED
@@ -0,0 +1,38 @@
1
+ import cookie from 'cookie';
2
+ import signature from 'cookie-signature';
3
+ import type { Request } from 'express';
4
+
5
+ export type CookieSecure = boolean | 'auto' | ((req: Request) => boolean);
6
+
7
+ export function shouldSecureCookie(req: Request, secure: CookieSecure): boolean {
8
+ if (typeof secure === 'function') {
9
+ return secure(req);
10
+ }
11
+
12
+ if (secure === 'auto') {
13
+ return req.protocol === 'https';
14
+ }
15
+
16
+ return secure;
17
+ }
18
+
19
+ export function getSessionIdFromCookie(
20
+ req: Request,
21
+ cookieName: string,
22
+ secrets: string[],
23
+ ): string | null {
24
+ const cookies = cookie.parse(req.headers.cookie ?? '');
25
+ const sessionCookie = cookies[cookieName];
26
+
27
+ if (!sessionCookie) return null;
28
+
29
+ // Try each secret until we find one that works.
30
+ for (const secret of secrets) {
31
+ const value = signature.unsign(sessionCookie, secret);
32
+ if (value !== false) {
33
+ return value;
34
+ }
35
+ }
36
+
37
+ return null;
38
+ }