@_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.
@@ -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: (_a = this.config.server) === null || _a === void 0 ? void 0 : _a.vite,
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 ((_b = mainEntry === null || mainEntry === void 0 ? void 0 : mainEntry.css) === null || _b === void 0 ? void 0 : _b[0]) {
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 = (_c = this.config.server) === null || _c === void 0 ? void 0 : _c.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.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
- }));
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
- // 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')));
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 (!((_d = this.config.server) === null || _d === void 0 ? void 0 : _d.apiOnly)) {
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
- indexing = this.indexPackageBackendProviders(pkg, warnIfNotFound).then(() => undefined);
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.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);
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
- this.noCache(response);
1394
- const result = await this.api.process(request, response, method);
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
- 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);
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 (shapeProvider[method]) {
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
- 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}`);
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
- console.warn(`Error whilst calling ${method}() in provider ${Object.getPrototypeOf(genericBackendProvider).constructor.name} of package ${pkg}:\n`, e);
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