@sasjs/server 3.7.0 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ var supertest_1 = __importDefault(require("supertest"));
43
+ var app_1 = __importDefault(require("../../../app"));
44
+ var utils_1 = require("../../../utils");
45
+ /**
46
+ * A path nothing claims is a 404, not a refused CSRF check (sasjs/server#495).
47
+ *
48
+ * The check hung off the web router's mount, so it ran before any route was
49
+ * matched: a POST to a path no route claims came back 400 'Invalid CSRF token!'
50
+ * - the same answer as a real request with a stale token. A caller reading that
51
+ * response cannot tell a typo in the URL from a token problem, and a frontend
52
+ * developer reading it sees a CSRF error for a URL that simply does not exist.
53
+ *
54
+ * The check stays on in desktop mode. It is the only cross-site defence that
55
+ * mode has - authenticateToken hands every request the fixed local user without
56
+ * asking for a credential, which is exactly what makes the session-bound token
57
+ * load bearing: without it, any page the user's browser has open can drive a
58
+ * POST at the local server. Fixing the 404-versus-400 confusion does not need
59
+ * the check dropped, so it is kept.
60
+ */
61
+ describe('CSRF and routing order', function () {
62
+ var app;
63
+ var mode = process.env.MODE;
64
+ beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
65
+ return __generator(this, function (_a) {
66
+ switch (_a.label) {
67
+ case 0: return [4 /*yield*/, app_1.default];
68
+ case 1:
69
+ app = _a.sent();
70
+ return [2 /*return*/];
71
+ }
72
+ });
73
+ }); });
74
+ afterEach(function () {
75
+ process.env.MODE = mode;
76
+ });
77
+ describe('server mode', function () {
78
+ beforeEach(function () {
79
+ process.env.MODE = utils_1.ModeType.Server;
80
+ });
81
+ it('answers a POST to an unclaimed path with 404', function () { return __awaiter(void 0, void 0, void 0, function () {
82
+ var response;
83
+ return __generator(this, function (_a) {
84
+ switch (_a.label) {
85
+ case 0: return [4 /*yield*/, (0, supertest_1.default)(app)
86
+ .post('/NotAWebRoute')
87
+ .send({ anything: true })];
88
+ case 1:
89
+ response = _a.sent();
90
+ expect(response.status).toBe(404);
91
+ expect(response.text).not.toContain('Invalid CSRF token!');
92
+ return [2 /*return*/];
93
+ }
94
+ });
95
+ }); });
96
+ it('still refuses a state-changing request that carries no token', function () { return __awaiter(void 0, void 0, void 0, function () {
97
+ var response;
98
+ return __generator(this, function (_a) {
99
+ switch (_a.label) {
100
+ case 0: return [4 /*yield*/, (0, supertest_1.default)(app)
101
+ .post('/SASLogon/login')
102
+ .send({ username: 'someone', password: 'something' })];
103
+ case 1:
104
+ response = _a.sent();
105
+ expect(response.status).toBe(400);
106
+ expect(response.text).toContain('Invalid CSRF token!');
107
+ return [2 /*return*/];
108
+ }
109
+ });
110
+ }); });
111
+ });
112
+ describe('desktop mode', function () {
113
+ beforeEach(function () {
114
+ process.env.MODE = utils_1.ModeType.Desktop;
115
+ });
116
+ it('answers a POST to an unclaimed path with 404', function () { return __awaiter(void 0, void 0, void 0, function () {
117
+ var response;
118
+ return __generator(this, function (_a) {
119
+ switch (_a.label) {
120
+ case 0: return [4 /*yield*/, (0, supertest_1.default)(app)
121
+ .post('/NotAWebRoute')
122
+ .send({ anything: true })];
123
+ case 1:
124
+ response = _a.sent();
125
+ expect(response.status).toBe(404);
126
+ return [2 /*return*/];
127
+ }
128
+ });
129
+ }); });
130
+ it('still refuses a login that carries no token', function () { return __awaiter(void 0, void 0, void 0, function () {
131
+ var response;
132
+ return __generator(this, function (_a) {
133
+ switch (_a.label) {
134
+ case 0: return [4 /*yield*/, (0, supertest_1.default)(app)
135
+ .post('/SASLogon/login')
136
+ .send({ username: 'someone', password: 'something' })];
137
+ case 1:
138
+ response = _a.sent();
139
+ expect(response.status).toBe(400);
140
+ expect(response.text).toContain('Invalid CSRF token!');
141
+ return [2 /*return*/];
142
+ }
143
+ });
144
+ }); });
145
+ });
146
+ });
@@ -8,7 +8,6 @@ var sas9_web_1 = __importDefault(require("./sas9-web"));
8
8
  var sasviya_web_1 = __importDefault(require("./sasviya-web"));
9
9
  var web_1 = __importDefault(require("./web"));
10
10
  var utils_1 = require("../../utils");
11
- var middlewares_1 = require("../../middlewares");
12
11
  var router = express_1.default.Router();
13
12
  var MOCK_SERVERTYPE = process.env.MOCK_SERVERTYPE;
14
13
  switch (MOCK_SERVERTYPE) {
@@ -21,7 +20,11 @@ switch (MOCK_SERVERTYPE) {
21
20
  break;
22
21
  }
23
22
  default: {
24
- router.use('/', middlewares_1.csrfProtection, web_1.default);
23
+ // The CSRF check is attached to the routes that change state (see
24
+ // webRouter's POSTs) rather than to the mount: a mount-level check refuses a
25
+ // POST to a path no route claims, answering 400 where the truth is 404
26
+ // (sasjs/server#495).
27
+ router.use('/', web_1.default);
25
28
  }
26
29
  }
27
30
  exports.default = router;
@@ -71,7 +71,7 @@ webRouter.get('/', function (req, res) { return __awaiter(void 0, void 0, void 0
71
71
  }
72
72
  });
73
73
  }); });
74
- webRouter.post('/SASLogon/login', middlewares_2.desktopRestrict, function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
74
+ webRouter.post('/SASLogon/login', middlewares_2.csrfProtection, middlewares_2.desktopRestrict, function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
75
75
  var _a, error, body, lockoutRemaining, response, err_1;
76
76
  return __generator(this, function (_b) {
77
77
  switch (_b.label) {
@@ -112,7 +112,7 @@ webRouter.post('/SASLogon/login', middlewares_2.desktopRestrict, function (req,
112
112
  }
113
113
  });
114
114
  }); });
115
- webRouter.post('/SASLogon/authorize', middlewares_2.desktopRestrict, middlewares_2.authenticateAccessToken, function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
115
+ webRouter.post('/SASLogon/authorize', middlewares_2.csrfProtection, middlewares_2.desktopRestrict, middlewares_2.authenticateAccessToken, function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
116
116
  var _a, error, body, response, err_2;
117
117
  return __generator(this, function (_b) {
118
118
  switch (_b.label) {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BUILD_INFO = void 0;
4
4
  // Generated by scripts/generateBuildInfo.ts. Do not edit by hand.
5
5
  exports.BUILD_INFO = {
6
- version: "v3.7.0",
7
- gitHash: "d1cc0c9",
8
- buildDate: "2026-11-01T21:13:10.033Z"
6
+ version: "v3.7.1",
7
+ gitHash: "eb89808",
8
+ buildDate: "2026-11-01T22:27:31.287Z"
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sasjs/server",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "description": "NodeJS wrapper for calling the SAS binary executable",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/sasjs/server",