@_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.
@@ -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: (_a = this.config.server) === null || _a === void 0 ? void 0 : _a.vite,
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 ((_b = mainEntry === null || mainEntry === void 0 ? void 0 : mainEntry.css) === null || _b === void 0 ? void 0 : _b[0]) {
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 = (_c = this.config.server) === null || _c === void 0 ? void 0 : _c.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.use('/uploads', express.static('./data/uploads', {
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
- // Scoped-package variants (@scope/pkg). Register BEFORE the unscoped
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 (!((_d = this.config.server) === null || _d === void 0 ? void 0 : _d.apiOnly)) {
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
- indexing = this.indexPackageBackendProviders(pkg, warnIfNotFound).then(() => undefined);
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.noCache(response);
1369
- await this.initRequest(request, response);
1370
- let { pkg, method } = request.params;
1371
- let { args } = JSONParser.parseObject(request.body);
1372
- return this.callBackendMethod(pkg, method, args, request, response).then((result) => {
1373
- //some methods of backend providers may choose to work with request/response directly and will not return anything
1374
- //so only if a result is returned
1375
- if (typeof result !== 'undefined') {
1376
- //do we convert it to JSON and send it to the frontend
1377
- this.sendJson(response, result);
1378
- }
1379
- else {
1380
- //in other cases, we still need to close the request and send an empty response
1381
- if (!response.headersSent) {
1382
- this.sendJson(response, null);
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
- this.noCache(response);
1394
- const result = await this.api.process(request, response, method);
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
- await this.initRequest(request, response);
1421
- let { pkg, shape, method } = request.params;
1422
- let { shapeURI, instanceNode, args } = JSONParser.parseObject(request.body);
1423
- if (!shapeURI) {
1424
- if ((_a = request.query) === null || _a === void 0 ? void 0 : _a.shapeURI) {
1425
- shapeURI = (_b = request.query) === null || _b === void 0 ? void 0 : _b.shapeURI;
1426
- }
1427
- else {
1428
- response.status(500).send({
1429
- error: 'Invalid server call request: ' + request.originalUrl,
1430
- });
1431
- console.warn(paint('red', 'Invalid server call request: ' + request.originalUrl));
1432
- return;
1433
- }
1434
- }
1435
- return this.callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response).then((result) => {
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 (shapeProvider[method]) {
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
- if (!genericBackendProvider[method]) {
1944
- console.warn(`Generic provider '${Object.getPrototypeOf(genericBackendProvider).constructor.name}' of ${pkg} does not have a method called ${method}`);
1945
- throw new ServerCallError(501, `No provider for ${pkg}/${method}`);
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
- console.warn(`Error whilst calling ${method}() in provider ${Object.getPrototypeOf(genericBackendProvider).constructor.name} of package ${pkg}:\n`, e);
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