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