@sasjs/server 3.6.2 → 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.
- package/README.md +39 -33
- package/build/src/e2e/studio.spec.js +98 -3
- package/build/src/routes/api/spec/csrfRouting.spec.js +146 -0
- package/build/src/routes/web/index.js +5 -2
- package/build/src/routes/web/web.js +2 -2
- package/build/src/utils/buildInfo.generated.js +3 -3
- package/package.json +2 -2
- package/web/build/index.bundle.js +24 -24
package/README.md
CHANGED
|
@@ -19,55 +19,61 @@ SASjs Server is available in two modes - Desktop (without authentication) and Se
|
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Two channels - the npm package and the standalone executable. Both read the same
|
|
23
|
+
environment variables and serve the same API.
|
|
23
24
|
|
|
24
|
-
###
|
|
25
|
+
### npm
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npx @sasjs/server@3.6.2
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install it once and run `sasjs-server` from then on:
|
|
27
32
|
|
|
28
33
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
npm install -g @sasjs/server
|
|
35
|
+
sasjs-server
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
Requires Node 22 or later. Useful where Node is already managed by the platform,
|
|
39
|
+
and the only channel that runs on arm64 (Apple Silicon, Graviton, a Pi), because
|
|
40
|
+
the standalone executables are built for x64 only.
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
Three things to know before choosing it:
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
- **Pin the version**, as above: an unpinned `npx @sasjs/server` re-resolves
|
|
45
|
+
`latest` on every run, so a restart becomes a silent upgrade.
|
|
46
|
+
- **`.env` is read from the working directory.** `dotenv.config()` is relative to
|
|
47
|
+
where the process starts, so run it from the project directory you want the
|
|
48
|
+
server to own (that directory also gets the `./sasjs_root`). Under `npx` the
|
|
49
|
+
package itself lives in an evictable `~/.npm/_npx/<hash>` cache, so an `.env`
|
|
50
|
+
placed there would be lost - it belongs beside the command, not the code.
|
|
51
|
+
- **It starts in desktop mode**, the single-user mode. The `RUN_AS` check still
|
|
52
|
+
refuses to run as root, so install it under the account that should own the SAS
|
|
53
|
+
sessions rather than starting it with `sudo`.
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
Installing the package runs no install script, so it makes no network call of its
|
|
56
|
+
own.
|
|
41
57
|
|
|
42
|
-
|
|
43
|
-
- Path to a filesystem location for Stored Programs and temporary files
|
|
58
|
+
### Standalone executable
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
Download the relevant package from the [releases](https://github.com/sasjs/server/releases)
|
|
61
|
+
page, or fetch it programmatically (linux):
|
|
46
62
|
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
```bash
|
|
64
|
+
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
|
65
|
+
unzip linux.zip
|
|
66
|
+
```
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`sasjs-server`. Useful where Node is already managed by the platform, and the
|
|
54
|
-
only channel that runs on arm64 (Apple Silicon, Graviton, a Pi), because the
|
|
55
|
-
standalone executables are built for x64 only.
|
|
68
|
+
Then launch `./api-linux` (on windows, double click `api-win.exe` - no
|
|
69
|
+
administrative privileges are needed) and answer the two prompts, unless the ENV
|
|
70
|
+
vars are set:
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
- Location of your `sas.exe` / `sas.sh` executable
|
|
73
|
+
- Path to a filesystem location for Stored Programs and temporary files
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
every run, so a restart becomes a silent upgrade. The examples above pin it.
|
|
62
|
-
- **`.env` is read from the working directory.** `dotenv.config()` is relative to
|
|
63
|
-
where the process starts, so run it from the project directory you want the
|
|
64
|
-
server to own (that directory also gets the `./sasjs_root`). Under `npx` the
|
|
65
|
-
package itself lives in an evictable `~/.npm/_npx/<hash>` cache, so an `.env`
|
|
66
|
-
placed there would be lost - it belongs beside the command, not the code.
|
|
75
|
+
## Deployment options
|
|
67
76
|
|
|
68
|
-
The Node channel starts in desktop mode, which is the single-user mode; the
|
|
69
|
-
`RUN_AS` check still refuses to run as root, so install it under the account that
|
|
70
|
-
should own the SAS sessions rather than starting it with `sudo`.
|
|
71
77
|
- **Embedded** - the server's pieces are also used inside other SASjs tooling,
|
|
72
78
|
such as the VS Code extension, and the mock backend runs in the browser for
|
|
73
79
|
front-end development without a server at all.
|
|
@@ -98,6 +98,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
98
98
|
* the unit-test run deliberately excludes.
|
|
99
99
|
*/
|
|
100
100
|
var path_1 = __importDefault(require("path"));
|
|
101
|
+
var fs_1 = require("fs");
|
|
101
102
|
var promises_1 = require("fs/promises");
|
|
102
103
|
var mongoose_1 = __importDefault(require("mongoose"));
|
|
103
104
|
var mongodb_memory_server_1 = require("mongodb-memory-server");
|
|
@@ -140,7 +141,11 @@ describe('Studio in a browser', function () {
|
|
|
140
141
|
case 2:
|
|
141
142
|
_a.sent();
|
|
142
143
|
_a.label = 3;
|
|
143
|
-
case 3:
|
|
144
|
+
case 3:
|
|
145
|
+
// The screenshots folder is gitignored, so a fresh checkout has no such
|
|
146
|
+
// directory and a failure screenshot would fail on the write itself.
|
|
147
|
+
(0, fs_1.mkdirSync)(screenshotDir, { recursive: true });
|
|
148
|
+
return [4 /*yield*/, (0, utils_1.fileExists)(path_1.default.join((0, utils_2.getWebBuildFolder)(), 'index.bundle.js'))];
|
|
144
149
|
case 4:
|
|
145
150
|
if (!(_a.sent())) {
|
|
146
151
|
throw new Error("No web build at ".concat((0, utils_2.getWebBuildFolder)(), ". Run `npm run build` in web/ before the browser test."));
|
|
@@ -185,7 +190,10 @@ describe('Studio in a browser', function () {
|
|
|
185
190
|
// An admin passes every route check, which keeps this test about the
|
|
186
191
|
// editor rather than about the permission model.
|
|
187
192
|
expect(dbUser.isAdmin).toBe(true);
|
|
188
|
-
|
|
193
|
+
// The fixture is a .sas program, and this suite never executes code for
|
|
194
|
+
// real - the log case fulfils the execute request in the browser - so one
|
|
195
|
+
// runtime, the one the fixture needs, keeps the editor from switching it.
|
|
196
|
+
process.runTimes = [utils_2.RunTimeType.SAS];
|
|
189
197
|
process.nodeLoc = process.execPath;
|
|
190
198
|
return [4 /*yield*/, app];
|
|
191
199
|
case 13:
|
|
@@ -200,7 +208,15 @@ describe('Studio in a browser', function () {
|
|
|
200
208
|
return [4 /*yield*/, browser.newPage({ viewport: { width: 1280, height: 800 } })];
|
|
201
209
|
case 15:
|
|
202
210
|
page = _a.sent();
|
|
203
|
-
page.on('pageerror', function (error) {
|
|
211
|
+
page.on('pageerror', function (error) {
|
|
212
|
+
// A request the app aborts surfaces here as an unhandled rejection with
|
|
213
|
+
// axios's "Canceled: Canceled". That is a request superseded by a newer one
|
|
214
|
+
// - the editor re-syncing, a tab switching - not a fault in the tree, which
|
|
215
|
+
// is what this collector exists to catch.
|
|
216
|
+
if (String(error).includes('Canceled: Canceled'))
|
|
217
|
+
return;
|
|
218
|
+
pageErrors.push(String(error));
|
|
219
|
+
});
|
|
204
220
|
page.on('request', function (request) {
|
|
205
221
|
var _a;
|
|
206
222
|
if (!request.url().includes('/SASjsApi/drive/fileTree'))
|
|
@@ -576,4 +592,83 @@ describe('Studio in a browser', function () {
|
|
|
576
592
|
}
|
|
577
593
|
});
|
|
578
594
|
}); }, 60000);
|
|
595
|
+
it('lists the errors, warnings and notable notes of a SAS log, and hides the notes on request', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
596
|
+
var log, _a, _b, _c, stored;
|
|
597
|
+
return __generator(this, function (_d) {
|
|
598
|
+
switch (_d.label) {
|
|
599
|
+
case 0:
|
|
600
|
+
log = [
|
|
601
|
+
'NOTE: SAS (r) Proprietary Software 9.4 (TS1M9 MBCS3170)',
|
|
602
|
+
'1 data want;',
|
|
603
|
+
'2 x = age / 0;',
|
|
604
|
+
'3 run;',
|
|
605
|
+
'WARNING: Division by zero detected at line 3 column 11.',
|
|
606
|
+
'NOTE: Variable y is uninitialized.',
|
|
607
|
+
'ERROR Libref X is not assigned.',
|
|
608
|
+
'NOTE 49-169: The meaning of an identifier after a quoted string may change.',
|
|
609
|
+
'ERROR: File WORK.MISSING_DS.DATA does not exist.',
|
|
610
|
+
''
|
|
611
|
+
].join('\n');
|
|
612
|
+
return [4 /*yield*/, page.route('**/SASjsApi/code/execute', function (route) {
|
|
613
|
+
return route.fulfill({
|
|
614
|
+
status: 200,
|
|
615
|
+
contentType: 'text/plain',
|
|
616
|
+
body: ['<html></html>', log, ''].join('SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784')
|
|
617
|
+
});
|
|
618
|
+
})
|
|
619
|
+
// The fixture program is a .sas file, so opening it selects the SAS runtime
|
|
620
|
+
// and the log pane will render the parsed shape.
|
|
621
|
+
];
|
|
622
|
+
case 1:
|
|
623
|
+
_d.sent();
|
|
624
|
+
// The fixture program is a .sas file, so opening it selects the SAS runtime
|
|
625
|
+
// and the log pane will render the parsed shape.
|
|
626
|
+
return [4 /*yield*/, page.click('button:has-text("RUN")')];
|
|
627
|
+
case 2:
|
|
628
|
+
// The fixture program is a .sas file, so opening it selects the SAS runtime
|
|
629
|
+
// and the log pane will render the parsed shape.
|
|
630
|
+
_d.sent();
|
|
631
|
+
return [4 /*yield*/, page.waitForSelector('text=Errors (2)', { timeout: 30000 })];
|
|
632
|
+
case 3:
|
|
633
|
+
_d.sent();
|
|
634
|
+
_a = expect;
|
|
635
|
+
return [4 /*yield*/, page.isVisible('text=Warnings (1)')];
|
|
636
|
+
case 4:
|
|
637
|
+
_a.apply(void 0, [_d.sent()]).toBe(true);
|
|
638
|
+
_b = expect;
|
|
639
|
+
return [4 /*yield*/, page.isVisible('text=Notes (2)')];
|
|
640
|
+
case 5:
|
|
641
|
+
_b.apply(void 0, [_d.sent()]).toBe(true);
|
|
642
|
+
return [4 /*yield*/, page.screenshot({
|
|
643
|
+
path: path_1.default.join(screenshotDir, '02-log-issues.png')
|
|
644
|
+
})
|
|
645
|
+
// The switch hides them without another run.
|
|
646
|
+
];
|
|
647
|
+
case 6:
|
|
648
|
+
_d.sent();
|
|
649
|
+
// The switch hides them without another run.
|
|
650
|
+
return [4 /*yield*/, page.click('[aria-label="Hide notable notes"]')];
|
|
651
|
+
case 7:
|
|
652
|
+
// The switch hides them without another run.
|
|
653
|
+
_d.sent();
|
|
654
|
+
return [4 /*yield*/, page.waitForSelector('text=Notes (2)', {
|
|
655
|
+
state: 'detached',
|
|
656
|
+
timeout: 30000
|
|
657
|
+
})];
|
|
658
|
+
case 8:
|
|
659
|
+
_d.sent();
|
|
660
|
+
_c = expect;
|
|
661
|
+
return [4 /*yield*/, page.isVisible('text=Errors (2)')];
|
|
662
|
+
case 9:
|
|
663
|
+
_c.apply(void 0, [_d.sent()]).toBe(true);
|
|
664
|
+
return [4 /*yield*/, page.evaluate(function () {
|
|
665
|
+
return window.localStorage.getItem('studio-settings');
|
|
666
|
+
})];
|
|
667
|
+
case 10:
|
|
668
|
+
stored = _d.sent();
|
|
669
|
+
expect(JSON.parse(stored)['log.notableNotes']).toBe(false);
|
|
670
|
+
return [2 /*return*/];
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
}); }, 120000);
|
|
579
674
|
});
|
|
@@ -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
|
-
|
|
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
|
-
gitHash: "
|
|
8
|
-
buildDate: "2026-
|
|
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.
|
|
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"express-session": "^1.19.0",
|
|
36
36
|
"helmet": "^8.3.0",
|
|
37
37
|
"http-headers-validation": "^0.0.2",
|
|
38
|
-
"joi": "^18.
|
|
38
|
+
"joi": "^18.3.1",
|
|
39
39
|
"jose": "5.10.0",
|
|
40
40
|
"jsonwebtoken": "^9.0.3",
|
|
41
41
|
"ldapjs": "2.3.3",
|