@_linked/server 2.20.0 → 2.22.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/CHANGELOG.md +39 -0
- package/lib/esm/backend.js +70 -11
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/shapes/LincdAPI.js +40 -11
- package/lib/esm/shapes/LincdAPI.js.map +1 -1
- package/lib/esm/shapes/LinkedServer.d.ts +48 -4
- package/lib/esm/shapes/LinkedServer.js +231 -86
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/filestores/LocalFileStore.d.ts +16 -0
- package/lib/esm/shapes/filestores/LocalFileStore.js +77 -8
- package/lib/esm/shapes/filestores/LocalFileStore.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +58 -0
- package/lib/esm/utils/queryPlane.js +208 -0
- package/lib/esm/utils/queryPlane.js.map +1 -0
- package/lib/esm/utils/rawQueries.d.ts +24 -0
- package/lib/esm/utils/rawQueries.js +42 -0
- package/lib/esm/utils/rawQueries.js.map +1 -0
- package/lib/esm/utils/rpcExposure.d.ts +53 -0
- package/lib/esm/utils/rpcExposure.js +199 -0
- package/lib/esm/utils/rpcExposure.js.map +1 -0
- package/package.json +6 -3
|
@@ -26,6 +26,7 @@ import { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';
|
|
|
26
26
|
import { Server } from '@_linked/server-utils/utils/Server';
|
|
27
27
|
import { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';
|
|
28
28
|
import { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';
|
|
29
|
+
import { getCallContext, httpCallContext, runAsSystem, runInHttpContext, runWithCallContext, } from '@_linked/server-utils/utils/CallContext';
|
|
29
30
|
import { Shape } from '@_linked/core/shapes/Shape';
|
|
30
31
|
import { LinkedErrorLogging } from '@_linked/core/utils/LinkedErrorLogging';
|
|
31
32
|
import { FileStorePurposes, LinkedFileStorage, } from '@_linked/core/utils/LinkedFileStorage';
|
|
@@ -114,6 +115,8 @@ import { indexShapesIntoMemory } from '../utils/Shapes.js';
|
|
|
114
115
|
import { materializeShapes } from '../utils/syncShapes.js';
|
|
115
116
|
import { installSpaFallback, repinSpaFallback, } from '../utils/spaFallback.js';
|
|
116
117
|
import { LincdAPI } from './LincdAPI.js';
|
|
118
|
+
import { getRpcExposureMode, isGenericRpcDisabled, logSafe, resolveCallable, setRpcExposureMode, warnUndeclaredCall, } from '../utils/rpcExposure.js';
|
|
119
|
+
import { setRawQueriesMode } from '../utils/rawQueries.js';
|
|
117
120
|
//prevent errors in node.js when (s)css files are imported in js
|
|
118
121
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
119
122
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
@@ -148,6 +151,48 @@ events.EventEmitter.prototype.setMaxListeners(500);
|
|
|
148
151
|
// global['document'] = document;
|
|
149
152
|
global['reactStaticRenderer'] = renderToStaticMarkup;
|
|
150
153
|
autoLoadOntologyData(true);
|
|
154
|
+
/**
|
|
155
|
+
* How RPC arguments treat a `{__sc}` (live Shape class) marker. No call takes a
|
|
156
|
+
* Shape class from the client: logged in 'warn', refused (400) in 'enforce'.
|
|
157
|
+
*/
|
|
158
|
+
function rpcShapeClassRevival() {
|
|
159
|
+
return getRpcExposureMode() === 'enforce' ? 'reject' : 'warn';
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Serve the local upload folder at `/uploads` (where `LocalFileStore` files are
|
|
163
|
+
* reachable), with the headers for untrusted files.
|
|
164
|
+
*/
|
|
165
|
+
export function mountUploads(app, folder = './data/uploads', maxAge = 1000 * 60 * 60 * 24 * 365) {
|
|
166
|
+
app.use('/uploads', express.static(folder, {
|
|
167
|
+
maxAge, // cache for a long time
|
|
168
|
+
immutable: true, // Suggest that the content won't change
|
|
169
|
+
// uploaded files are user content: never sniffed, never run as a page
|
|
170
|
+
setHeaders: setUntrustedFileHeaders,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Express middleware: run the rest of the chain in the request's http context.
|
|
175
|
+
* `LinkedServer.start` installs it first, and again right after the JSON body
|
|
176
|
+
* parser (whose stream callbacks do not carry the context).
|
|
177
|
+
*/
|
|
178
|
+
export function enterRequestContext(req, res, next) {
|
|
179
|
+
runInHttpContext(req, res, next);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Headers for files users uploaded (`/uploads`, resized copies): the browser
|
|
183
|
+
* must not guess a type from the bytes, and a document served from them runs
|
|
184
|
+
* sandboxed (no scripts, an opaque origin), so an uploaded HTML or SVG file
|
|
185
|
+
* cannot act on the app's origin.
|
|
186
|
+
*/
|
|
187
|
+
export function setUntrustedFileHeaders(res) {
|
|
188
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
189
|
+
res.setHeader('Content-Security-Policy', 'sandbox');
|
|
190
|
+
}
|
|
191
|
+
/** Whether the current HTTP call carries a session. */
|
|
192
|
+
function hasSession(request) {
|
|
193
|
+
var _a;
|
|
194
|
+
return !!((_a = request === null || request === void 0 ? void 0 : request.linkedAuth) === null || _a === void 0 ? void 0 : _a.userAccount);
|
|
195
|
+
}
|
|
151
196
|
// Named explicitly: tsc emits `let X = class X`, and any later esbuild pass over that JS (Vite's
|
|
152
197
|
// SSR `define` replacement runs one on every file mentioning a defined `process.env.*`) renames
|
|
153
198
|
// the inner binding to `X2`, which would otherwise become this shape's IRI.
|
|
@@ -366,8 +411,12 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
366
411
|
setTimeout(() => process.exit(1), 1000).unref();
|
|
367
412
|
}
|
|
368
413
|
async startBoot() {
|
|
369
|
-
var _a, _b, _c, _d;
|
|
414
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
370
415
|
this.initPackage();
|
|
416
|
+
// 'warn' (default) or 'enforce'; see utils/rpcExposure.
|
|
417
|
+
setRpcExposureMode((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.server) === null || _b === void 0 ? void 0 : _b.rpcExposure);
|
|
418
|
+
// 'session' (default) or 'off'; see utils/rawQueries.
|
|
419
|
+
setRawQueriesMode((_d = (_c = this.config) === null || _c === void 0 ? void 0 : _c.server) === null || _d === void 0 ? void 0 : _d.rawQueries);
|
|
371
420
|
// Static assets should come from the static store URL (versioned path),
|
|
372
421
|
// not the upload store URL. An explicit STATIC_ACCESS_URL still wins;
|
|
373
422
|
// otherwise the release manifest `linked build-app` wrote next to the
|
|
@@ -396,7 +445,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
396
445
|
// one left on disk by an earlier `vite build` would otherwise swap in
|
|
397
446
|
// stale built CSS while Vite serves the live styles.
|
|
398
447
|
const servingMode = resolveViteServingMode({
|
|
399
|
-
viteConfig: (
|
|
448
|
+
viteConfig: (_e = this.config.server) === null || _e === void 0 ? void 0 : _e.vite,
|
|
400
449
|
nodeEnv: process.env.NODE_ENV,
|
|
401
450
|
readManifest: () => {
|
|
402
451
|
try {
|
|
@@ -422,7 +471,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
422
471
|
this.assets['main.js'] = staticAsset(`/bundles/${mainEntry.file}`);
|
|
423
472
|
this.mainEntryIsModule = true;
|
|
424
473
|
}
|
|
425
|
-
if ((
|
|
474
|
+
if ((_f = mainEntry === null || mainEntry === void 0 ? void 0 : mainEntry.css) === null || _f === void 0 ? void 0 : _f[0]) {
|
|
426
475
|
this.assets['main.css'] = staticAsset(`/bundles/${mainEntry.css[0]}`);
|
|
427
476
|
}
|
|
428
477
|
}
|
|
@@ -461,8 +510,15 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
461
510
|
// origin: ['http://localhost:4001', 'https://www.mynd.site'],
|
|
462
511
|
// optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
|
|
463
512
|
// };
|
|
513
|
+
// Every request runs in its own http call context, from the first layer
|
|
514
|
+
// on: no handler (middleware, routers, error handlers) ever runs without
|
|
515
|
+
// one. The context is kept on the request, so a layer that resumes from a
|
|
516
|
+
// callback that lost it (a body parser's stream events) re-enters the same
|
|
517
|
+
// one below.
|
|
518
|
+
this.server.use(enterRequestContext);
|
|
464
519
|
//accept JSON bodies
|
|
465
520
|
this.server.use(createJsonBodyParser());
|
|
521
|
+
this.server.use(enterRequestContext);
|
|
466
522
|
// this.server.use(cors(corsOptions));
|
|
467
523
|
//
|
|
468
524
|
// //compress server output with gzip,
|
|
@@ -473,7 +529,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
473
529
|
// (`linked start --vite`), the orchestrator passes the Vite handle
|
|
474
530
|
// through config.server.viteMiddleware. We mount it and skip the
|
|
475
531
|
// entire webpack-dev-middleware setup below.
|
|
476
|
-
const viteMiddleware = (
|
|
532
|
+
const viteMiddleware = (_g = this.config.server) === null || _g === void 0 ? void 0 : _g.viteMiddleware;
|
|
477
533
|
if (viteMiddleware) {
|
|
478
534
|
this.server.use(viteMiddleware);
|
|
479
535
|
}
|
|
@@ -490,10 +546,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
490
546
|
maxAge: oneYear, // Tell browser to cache for 1 year
|
|
491
547
|
immutable: true, // Suggest that the content won't change
|
|
492
548
|
}));
|
|
493
|
-
this.server
|
|
494
|
-
maxAge: oneYear, // Tell browser to cache for 1 year
|
|
495
|
-
immutable: true, // Suggest that the content won't change
|
|
496
|
-
}));
|
|
549
|
+
mountUploads(this.server, './data/uploads', oneYear);
|
|
497
550
|
this.server.use('/', express.static('./public/root'));
|
|
498
551
|
this.server.use('/favicon.ico', express.static('./public/favicon.ico', {
|
|
499
552
|
maxAge: oneMonth, // Tell browser to cache for 1 year
|
|
@@ -504,25 +557,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
504
557
|
this.server.get('/resized/*', async (req, res) => {
|
|
505
558
|
this.resizeImage(req, res);
|
|
506
559
|
});
|
|
507
|
-
|
|
508
|
-
// routes — Express `:pkg` won't consume slashes, so a call to
|
|
509
|
-
// `/call/@_linked/auth/signinDev` would otherwise fall through to the
|
|
510
|
-
// 3-segment `:pkg/:shape/:method` route and be misinterpreted as a
|
|
511
|
-
// shape-method call (pkg=`@_linked`, shape=`auth`). These handlers
|
|
512
|
-
// recognise the `@scope/pkg` prefix and rebuild the full package name
|
|
513
|
-
// before dispatching to the same handler used for unscoped packages.
|
|
514
|
-
this.server.post('/call/@:scope/:pkg/:method', this.handleErrorsJson(async (req, res) => {
|
|
515
|
-
req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
|
|
516
|
-
return this.processBackendMethodCall(req, res);
|
|
517
|
-
}));
|
|
518
|
-
this.server.post('/call/@:scope/:pkg/:shape/:method', this.handleErrorsJson(async (req, res) => {
|
|
519
|
-
req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
|
|
520
|
-
return this.processShapeMethodCall(req, res);
|
|
521
|
-
}));
|
|
522
|
-
this.server.post('/call/:pkg/:method', this.handleErrorsJson(async (req, res) => this.processBackendMethodCall(req, res)));
|
|
523
|
-
this.server.post('/call/:pkg/:shape/:method', this.handleErrorsJson(async (req, res) => this.processShapeMethodCall(req, res)));
|
|
524
|
-
this.server.post('/api/:method/:action?', this.handleErrorsJson(async (req, res) => this.processAPICall(req, res, 'post')));
|
|
525
|
-
this.server.get('/api/:method/:action?', this.handleErrorsJson(async (req, res) => this.processAPICall(req, res, 'get')));
|
|
560
|
+
registerCallRoutes(this.server, this);
|
|
526
561
|
// this.server.post(
|
|
527
562
|
// '/api/query/:method',
|
|
528
563
|
// this.handleErrorsJson(async (req, res) => this.processQuery(req, res)),
|
|
@@ -580,7 +615,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
580
615
|
// answering them with the client shell at status 200.
|
|
581
616
|
// An API-only backend (`linked start --api-only`) has no app to render:
|
|
582
617
|
// without the catch-all, page requests get express's plain 404.
|
|
583
|
-
if (!((
|
|
618
|
+
if (!((_h = this.config.server) === null || _h === void 0 ? void 0 : _h.apiOnly)) {
|
|
584
619
|
this.installSpaFallback();
|
|
585
620
|
}
|
|
586
621
|
//remove http(s):// and remove port :[port]
|
|
@@ -806,7 +841,9 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
806
841
|
}
|
|
807
842
|
let indexing = this.providerIndexing.get(pkg);
|
|
808
843
|
if (!indexing) {
|
|
809
|
-
|
|
844
|
+
// Loading a package's providers runs their constructors and setup hooks.
|
|
845
|
+
// That is system work, even when the first call to the package triggers it.
|
|
846
|
+
indexing = runAsSystem(() => this.indexPackageBackendProviders(pkg, warnIfNotFound), 'loading backend providers of ' + pkg).then(() => undefined);
|
|
810
847
|
this.providerIndexing.set(pkg, indexing);
|
|
811
848
|
}
|
|
812
849
|
try {
|
|
@@ -1098,6 +1135,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1098
1135
|
catch (err) {
|
|
1099
1136
|
console.warn('Could not cache resized image at ' + resizedKey + ': ' + err);
|
|
1100
1137
|
}
|
|
1138
|
+
setUntrustedFileHeaders(res);
|
|
1101
1139
|
res.type(extension || 'bin').send(resized);
|
|
1102
1140
|
}
|
|
1103
1141
|
async indexLincdPackage(pkg, warnIfNotFound = false) {
|
|
@@ -1142,14 +1180,14 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1142
1180
|
if (providerNotFound) {
|
|
1143
1181
|
// console.warn('Error loading ' + providerPath + ': ' + e.stack);
|
|
1144
1182
|
if (warnIfNotFound) {
|
|
1145
|
-
console.warn(paint('magenta', `Could not load package ${pkg}`, typeof module !== 'undefined' && typeof exports !== 'undefined'
|
|
1183
|
+
console.warn(paint('magenta', `Could not load package ${logSafe(pkg)}`, typeof module !== 'undefined' && typeof exports !== 'undefined'
|
|
1146
1184
|
? //@ts-ignore
|
|
1147
1185
|
' at ' + (await import.meta.resolve(pkg))
|
|
1148
1186
|
: ''));
|
|
1149
1187
|
}
|
|
1150
1188
|
}
|
|
1151
1189
|
else {
|
|
1152
|
-
console.warn(paint('red', `Error loading '${pkg}' ${typeof module !== 'undefined' && typeof exports !== 'undefined'
|
|
1190
|
+
console.warn(paint('red', `Error loading '${logSafe(pkg)}' ${typeof module !== 'undefined' && typeof exports !== 'undefined'
|
|
1153
1191
|
? //@ts-ignore
|
|
1154
1192
|
' at ' + (await import.meta.resolve(pkg))
|
|
1155
1193
|
: ''}: ${e.message}\n`), e.stack);
|
|
@@ -1239,7 +1277,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1239
1277
|
}
|
|
1240
1278
|
if (!load) {
|
|
1241
1279
|
if (warnIfNotFound) {
|
|
1242
|
-
console.warn(paint('magenta', `Could not find backend file of package ${pkg}.
|
|
1280
|
+
console.warn(paint('magenta', `Could not find backend file of package ${logSafe(pkg)}.
|
|
1243
1281
|
Check:\n
|
|
1244
1282
|
- Make sure backend.ts exists and is included in tsconfig.json\n
|
|
1245
1283
|
- Make sure the package name in src/package.ts matches the package name in package.json`));
|
|
@@ -1267,12 +1305,12 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1267
1305
|
if (isShapeProvider(provider)) {
|
|
1268
1306
|
shapeProviders.push(provider);
|
|
1269
1307
|
if (!Object.getOwnPropertyNames(provider).includes('shape')) {
|
|
1270
|
-
console.warn(paint('red', `${Object.getPrototypeOf(provider).constructor.name} in package ${pkg}
|
|
1308
|
+
console.warn(paint('red', `${Object.getPrototypeOf(provider).constructor.name} in package ${logSafe(pkg)}
|
|
1271
1309
|
is not properly linked to a shape. Use public shape = SomeShape.`));
|
|
1272
1310
|
}
|
|
1273
1311
|
}
|
|
1274
1312
|
else if (genericBackendProvider) {
|
|
1275
|
-
console.warn(`Package ${pkg} exports two generic backend providers. Only one will work`);
|
|
1313
|
+
console.warn(`Package ${logSafe(pkg)} exports two generic backend providers. Only one will work`);
|
|
1276
1314
|
}
|
|
1277
1315
|
else {
|
|
1278
1316
|
genericBackendProvider = provider;
|
|
@@ -1365,23 +1403,29 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1365
1403
|
repinSpaFallback(this.server);
|
|
1366
1404
|
}
|
|
1367
1405
|
async processBackendMethodCall(request, response) {
|
|
1368
|
-
this.
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
this.sendJson(response,
|
|
1406
|
+
// The whole call runs in its own context: `this.request` in a provider is
|
|
1407
|
+
// this request, however many calls are in flight.
|
|
1408
|
+
return runWithCallContext(httpCallContext(request, response), async () => {
|
|
1409
|
+
this.noCache(response);
|
|
1410
|
+
await this.initRequest(request, response);
|
|
1411
|
+
let { pkg, method } = request.params;
|
|
1412
|
+
let { args } = JSONParser.parseObject(request.body, {
|
|
1413
|
+
shapeClasses: rpcShapeClassRevival(),
|
|
1414
|
+
});
|
|
1415
|
+
return this.callBackendMethod(pkg, method, args, request, response).then((result) => {
|
|
1416
|
+
//some methods of backend providers may choose to work with request/response directly and will not return anything
|
|
1417
|
+
//so only if a result is returned
|
|
1418
|
+
if (typeof result !== 'undefined') {
|
|
1419
|
+
//do we convert it to JSON and send it to the frontend
|
|
1420
|
+
this.sendJson(response, result);
|
|
1383
1421
|
}
|
|
1384
|
-
|
|
1422
|
+
else {
|
|
1423
|
+
//in other cases, we still need to close the request and send an empty response
|
|
1424
|
+
if (!response.headersSent) {
|
|
1425
|
+
this.sendJson(response, null);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1385
1429
|
});
|
|
1386
1430
|
}
|
|
1387
1431
|
noCache(response) {
|
|
@@ -1390,15 +1434,16 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1390
1434
|
response.setHeader('Expires', '0');
|
|
1391
1435
|
}
|
|
1392
1436
|
async processAPICall(request, response, method) {
|
|
1393
|
-
|
|
1394
|
-
|
|
1437
|
+
return runWithCallContext(httpCallContext(request, response), async () => {
|
|
1438
|
+
this.noCache(response);
|
|
1439
|
+
await this.api.process(request, response, method);
|
|
1440
|
+
});
|
|
1395
1441
|
}
|
|
1396
1442
|
// async processQuery(request, response) {
|
|
1397
1443
|
//
|
|
1398
1444
|
// this.api.process(request, response);
|
|
1399
1445
|
// }
|
|
1400
1446
|
async processShapeMethodCall(request, response) {
|
|
1401
|
-
var _a, _b;
|
|
1402
1447
|
this.noCache(response);
|
|
1403
1448
|
// console.log('process shape call');
|
|
1404
1449
|
// response.on('finish', function() {
|
|
@@ -1417,30 +1462,37 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1417
1462
|
// console.log('header');
|
|
1418
1463
|
// console.log(response.statusCode);
|
|
1419
1464
|
// });
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
//we return json if something was returned or, if nothing was returned, we still close the request if the method has not accessed response itself already to send things over
|
|
1437
|
-
if (typeof result !== 'undefined' || !response.headersSent) {
|
|
1438
|
-
this.sendJson(response, result);
|
|
1465
|
+
return runWithCallContext(httpCallContext(request, response), async () => {
|
|
1466
|
+
var _a, _b;
|
|
1467
|
+
await this.initRequest(request, response);
|
|
1468
|
+
let { pkg, shape, method } = request.params;
|
|
1469
|
+
let { shapeURI, instanceNode, args } = JSONParser.parseObject(request.body, { shapeClasses: rpcShapeClassRevival() });
|
|
1470
|
+
if (!shapeURI) {
|
|
1471
|
+
if ((_a = request.query) === null || _a === void 0 ? void 0 : _a.shapeURI) {
|
|
1472
|
+
shapeURI = (_b = request.query) === null || _b === void 0 ? void 0 : _b.shapeURI;
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
response.status(500).send({
|
|
1476
|
+
error: 'Invalid server call request: ' + request.originalUrl,
|
|
1477
|
+
});
|
|
1478
|
+
console.warn(paint('red', 'Invalid server call request: ' + request.originalUrl));
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1439
1481
|
}
|
|
1482
|
+
return this.callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response).then((result) => {
|
|
1483
|
+
//we return json if something was returned or, if nothing was returned, we still close the request if the method has not accessed response itself already to send things over
|
|
1484
|
+
if (typeof result !== 'undefined' || !response.headersSent) {
|
|
1485
|
+
this.sendJson(response, result);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1440
1488
|
});
|
|
1441
1489
|
}
|
|
1442
1490
|
async callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response) {
|
|
1443
1491
|
var _a;
|
|
1492
|
+
// see callBackendMethod: outside any request a local call runs as system
|
|
1493
|
+
if (!request && !getCallContext()) {
|
|
1494
|
+
return runAsSystem(() => this.callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response));
|
|
1495
|
+
}
|
|
1444
1496
|
//- index module providers if not done yet
|
|
1445
1497
|
if (!this.shapeProviders.has(pkg)) {
|
|
1446
1498
|
await this.ensurePackageBackendProviders(pkg, true);
|
|
@@ -1478,20 +1530,27 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1478
1530
|
// misconfiguration. Answer 501 (via handleErrorsJson) rather than an empty
|
|
1479
1531
|
// 200; direct backend callers get a rejected ServerCallError.
|
|
1480
1532
|
if (!shapeProvider) {
|
|
1481
|
-
console.warn(`[LinkedServer] callShapeMethod: no provider for '${shapeURI}' ` +
|
|
1482
|
-
`(pkg '${pkg}', method '${method}').`);
|
|
1483
|
-
throw new ServerCallError(501, `No provider for ${pkg}/${method}`);
|
|
1533
|
+
console.warn(`[LinkedServer] callShapeMethod: no provider for '${logSafe(shapeURI)}' ` +
|
|
1534
|
+
`(pkg '${logSafe(pkg)}', method '${logSafe(method)}').`);
|
|
1535
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1484
1536
|
}
|
|
1485
1537
|
if (shapeProvider) {
|
|
1538
|
+
const resolution = resolveCallable(shapeProvider, method);
|
|
1539
|
+
if (resolution.status !== 'missing') {
|
|
1540
|
+
this.assertDispatchAllowed(pkg, method, shapeProvider, resolution, request, 'shape');
|
|
1541
|
+
}
|
|
1486
1542
|
//NOTE: if this is a direct call from backend to backend, we won't know the request & response here because those don't get passed to the Server utility
|
|
1487
1543
|
//if this is an issue, we need to see how we can get those back
|
|
1488
1544
|
if (request && response) {
|
|
1489
1545
|
//give the provider a chance to prepare for this request
|
|
1490
1546
|
//wrap the call in a promise and wait for it, because providers MAY return a promise
|
|
1491
1547
|
await Promise.resolve(shapeProvider.initRequest(request, response));
|
|
1548
|
+
if (resolution.status !== 'missing') {
|
|
1549
|
+
this.assertSessionForLevel(pkg, method, resolution, request);
|
|
1550
|
+
}
|
|
1492
1551
|
}
|
|
1493
1552
|
//see if the shapeProvider implements the called method
|
|
1494
|
-
if (
|
|
1553
|
+
if (resolution.status !== 'missing') {
|
|
1495
1554
|
//prepare the first argument
|
|
1496
1555
|
//we want to convert the instance node into an instance of the shape that this shapeProvider provides for
|
|
1497
1556
|
//let's find the shape class
|
|
@@ -1511,7 +1570,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1511
1570
|
return result;
|
|
1512
1571
|
}
|
|
1513
1572
|
catch (e) {
|
|
1514
|
-
console.warn(`Error whilst calling ${Object.getPrototypeOf(shapeProvider).constructor.name}.${method}(): `, e);
|
|
1573
|
+
console.warn(`Error whilst calling ${Object.getPrototypeOf(shapeProvider).constructor.name}.${logSafe(method)}(): `, e);
|
|
1515
1574
|
// Rethrow so the HTTP route answers with an error status (via
|
|
1516
1575
|
// handleErrorsJson) instead of `200 null`, and direct backend
|
|
1517
1576
|
// callers get a rejected promise.
|
|
@@ -1524,19 +1583,61 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1524
1583
|
}
|
|
1525
1584
|
}
|
|
1526
1585
|
else {
|
|
1527
|
-
console.warn(`${Object.getPrototypeOf(shapeProvider).constructor.name} does not have a method called ${method}`);
|
|
1528
|
-
throw new ServerCallError(501, `No provider for ${pkg}/${method}`);
|
|
1586
|
+
console.warn(`${Object.getPrototypeOf(shapeProvider).constructor.name} does not have a method called ${logSafe(method)}`);
|
|
1587
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1529
1588
|
}
|
|
1530
1589
|
}
|
|
1531
1590
|
}
|
|
1532
1591
|
catch (err) {
|
|
1533
1592
|
if (!providerMethodFailed && !ServerCallError.is(err)) {
|
|
1534
|
-
console.warn(`Error whilst trying to access provider of ${pkg}: `, err);
|
|
1593
|
+
console.warn(`Error whilst trying to access provider of ${logSafe(pkg)}: `, err);
|
|
1535
1594
|
}
|
|
1536
1595
|
throw err;
|
|
1537
1596
|
}
|
|
1538
1597
|
return null;
|
|
1539
1598
|
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Refuse a dispatch the exposure rules do not allow (see utils/rpcExposure).
|
|
1601
|
+
* Throws a 501 `ServerCallError`, the same answer as for a missing method.
|
|
1602
|
+
*
|
|
1603
|
+
* - reserved names: always, for HTTP and backend-to-backend calls alike;
|
|
1604
|
+
* - HTTP only: an internal method (in every mode), a generic call to a provider whose class sets
|
|
1605
|
+
* `static rpc = false`, and an undeclared method in 'enforce' mode
|
|
1606
|
+
* (in 'warn' mode it is logged once and runs).
|
|
1607
|
+
*/
|
|
1608
|
+
assertDispatchAllowed(pkg, method, provider, resolution, request, route) {
|
|
1609
|
+
var _a, _b;
|
|
1610
|
+
const className = (_b = (_a = Object.getPrototypeOf(provider)) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name;
|
|
1611
|
+
if (resolution.status === 'reserved') {
|
|
1612
|
+
console.warn(`[linked] refused call to reserved method ${logSafe(pkg)} ${className}.${logSafe(method)}` +
|
|
1613
|
+
(request ? '' : ' (backend-to-backend)'));
|
|
1614
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1615
|
+
}
|
|
1616
|
+
if (!request)
|
|
1617
|
+
return;
|
|
1618
|
+
if (resolution.status === 'internal') {
|
|
1619
|
+
console.warn(`[linked] refused call to internal method ${logSafe(pkg)} ${className}.${logSafe(method)}`);
|
|
1620
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (route === 'generic' && isGenericRpcDisabled(provider)) {
|
|
1623
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1624
|
+
}
|
|
1625
|
+
if (resolution.status === 'undeclared') {
|
|
1626
|
+
if (getRpcExposureMode() === 'enforce') {
|
|
1627
|
+
console.warn(`[linked] refused undeclared RPC ${logSafe(pkg)} ${className}.${logSafe(method)}`);
|
|
1628
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1629
|
+
}
|
|
1630
|
+
warnUndeclaredCall(pkg, resolution.owner, method, hasSession(request));
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/** A declared `'user'` method needs a session: 401 without one, in every mode. */
|
|
1634
|
+
assertSessionForLevel(pkg, method, resolution, request) {
|
|
1635
|
+
if (resolution.status === 'callable' &&
|
|
1636
|
+
resolution.level === 'user' &&
|
|
1637
|
+
!hasSession(request)) {
|
|
1638
|
+
throw new ServerCallError(401, 'Authentication required');
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1540
1641
|
handleErrors(fn) {
|
|
1541
1642
|
return async function (req, res, next) {
|
|
1542
1643
|
try {
|
|
@@ -1641,6 +1742,11 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1641
1742
|
return cssChunks.join('\n');
|
|
1642
1743
|
}
|
|
1643
1744
|
async render(req, res) {
|
|
1745
|
+
// Server-side rendering runs as the requesting user: a Server.call made
|
|
1746
|
+
// while rendering inherits this context rather than escalating to system.
|
|
1747
|
+
return runWithCallContext(httpCallContext(req, res), () => this.renderPage(req, res));
|
|
1748
|
+
}
|
|
1749
|
+
async renderPage(req, res) {
|
|
1644
1750
|
var _a, _b, _c, _d, _e, _f;
|
|
1645
1751
|
res.socket.on('error', (error) => {
|
|
1646
1752
|
console.error('Fatal socket error', error);
|
|
@@ -1929,6 +2035,11 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1929
2035
|
return { requestLD, requestObject };
|
|
1930
2036
|
}
|
|
1931
2037
|
async callBackendMethod(pkg, method, args, request, response) {
|
|
2038
|
+
// A backend-to-backend call outside any request runs as system. Inside a
|
|
2039
|
+
// request (SSR, a provider calling another) it inherits that request.
|
|
2040
|
+
if (!request && !getCallContext()) {
|
|
2041
|
+
return runAsSystem(() => this.callBackendMethod(pkg, method, args, request, response));
|
|
2042
|
+
}
|
|
1932
2043
|
if (!this.genericProviders.has(pkg)) {
|
|
1933
2044
|
await this.ensurePackageBackendProviders(pkg, true);
|
|
1934
2045
|
}
|
|
@@ -1937,13 +2048,15 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1937
2048
|
let result;
|
|
1938
2049
|
if (!genericBackendProvider) {
|
|
1939
2050
|
console.warn(`${paint('magenta', pkg)} does not have a generic backend provider. If you can edit this package, make sure 'backend.ts' is included in 'tsconfig.json' and that it exports a provider.`);
|
|
1940
|
-
throw new ServerCallError(501, `No provider for ${pkg}/${method}`);
|
|
2051
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1941
2052
|
}
|
|
1942
|
-
//test if there is a matching method in the backend provider
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2053
|
+
//test if there is a matching method in the backend provider, and whether it may be called
|
|
2054
|
+
const resolution = resolveCallable(genericBackendProvider, method);
|
|
2055
|
+
if (resolution.status === 'missing') {
|
|
2056
|
+
console.warn(`Generic provider '${Object.getPrototypeOf(genericBackendProvider).constructor.name}' of ${logSafe(pkg)} does not have a method called ${logSafe(method)}`);
|
|
2057
|
+
throw new ServerCallError(501, `No provider for ${logSafe(pkg)}/${logSafe(method)}`);
|
|
1946
2058
|
}
|
|
2059
|
+
this.assertDispatchAllowed(pkg, method, genericBackendProvider, resolution, request, 'generic');
|
|
1947
2060
|
try {
|
|
1948
2061
|
//TODO: remove init request.
|
|
1949
2062
|
//TODO: refactor this.request and this.response to a request parameter
|
|
@@ -1953,6 +2066,7 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1953
2066
|
//initialise the request for this specific provider
|
|
1954
2067
|
//wrap the call in a promise and wait for it, because providers MAY return a promise
|
|
1955
2068
|
await Promise.resolve(genericBackendProvider.initRequest(request, response));
|
|
2069
|
+
this.assertSessionForLevel(pkg, method, resolution, request);
|
|
1956
2070
|
}
|
|
1957
2071
|
// args.push(request);
|
|
1958
2072
|
// args.push(response);
|
|
@@ -1960,7 +2074,9 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1960
2074
|
result = await Promise.resolve(genericBackendProvider[method].apply(genericBackendProvider, args));
|
|
1961
2075
|
}
|
|
1962
2076
|
catch (e) {
|
|
1963
|
-
|
|
2077
|
+
if (ServerCallError.is(e) && e.status === 401)
|
|
2078
|
+
throw e;
|
|
2079
|
+
console.warn(`Error whilst calling ${logSafe(method)}() in provider ${Object.getPrototypeOf(genericBackendProvider).constructor.name} of package ${logSafe(pkg)}:\n`, e);
|
|
1964
2080
|
// error logging
|
|
1965
2081
|
LinkedErrorLogging.log(e);
|
|
1966
2082
|
// Rethrow so the HTTP route answers with an error status instead of `200 null`.
|
|
@@ -1978,4 +2094,33 @@ LinkedServer = __decorate([
|
|
|
1978
2094
|
__metadata("design:paramtypes", [Object])
|
|
1979
2095
|
], LinkedServer);
|
|
1980
2096
|
export { LinkedServer };
|
|
2097
|
+
/**
|
|
2098
|
+
* Register the RPC routes (`/call/...`) and the `LincdAPI` routes (`/api/...`)
|
|
2099
|
+
* of `linkedServer` on an express app.
|
|
2100
|
+
*
|
|
2101
|
+
* `LinkedServer.start()` calls this; tests call it on a bare express app so they
|
|
2102
|
+
* drive exactly the routes a running server has.
|
|
2103
|
+
*/
|
|
2104
|
+
export function registerCallRoutes(app, linkedServer) {
|
|
2105
|
+
const server = linkedServer;
|
|
2106
|
+
// Scoped-package variants (@scope/pkg). Register BEFORE the unscoped
|
|
2107
|
+
// routes — Express `:pkg` won't consume slashes, so a call to
|
|
2108
|
+
// `/call/@_linked/auth/signinDev` would otherwise fall through to the
|
|
2109
|
+
// 3-segment `:pkg/:shape/:method` route and be misinterpreted as a
|
|
2110
|
+
// shape-method call (pkg=`@_linked`, shape=`auth`). These handlers
|
|
2111
|
+
// recognise the `@scope/pkg` prefix and rebuild the full package name
|
|
2112
|
+
// before dispatching to the same handler used for unscoped packages.
|
|
2113
|
+
app.post('/call/@:scope/:pkg/:method', server.handleErrorsJson(async (req, res) => {
|
|
2114
|
+
req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
|
|
2115
|
+
return server.processBackendMethodCall(req, res);
|
|
2116
|
+
}));
|
|
2117
|
+
app.post('/call/@:scope/:pkg/:shape/:method', server.handleErrorsJson(async (req, res) => {
|
|
2118
|
+
req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
|
|
2119
|
+
return server.processShapeMethodCall(req, res);
|
|
2120
|
+
}));
|
|
2121
|
+
app.post('/call/:pkg/:method', server.handleErrorsJson(async (req, res) => server.processBackendMethodCall(req, res)));
|
|
2122
|
+
app.post('/call/:pkg/:shape/:method', server.handleErrorsJson(async (req, res) => server.processShapeMethodCall(req, res)));
|
|
2123
|
+
app.post('/api/:method/:action?', server.handleErrorsJson(async (req, res) => server.processAPICall(req, res, 'post')));
|
|
2124
|
+
app.get('/api/:method/:action?', server.handleErrorsJson(async (req, res) => server.processAPICall(req, res, 'get')));
|
|
2125
|
+
}
|
|
1981
2126
|
//# sourceMappingURL=LinkedServer.js.map
|