@_linked/server 2.20.0 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/lib/esm/backend.js +70 -11
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/shapes/LincdAPI.js +40 -11
- package/lib/esm/shapes/LincdAPI.js.map +1 -1
- package/lib/esm/shapes/LinkedServer.d.ts +48 -4
- package/lib/esm/shapes/LinkedServer.js +231 -86
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/filestores/LocalFileStore.d.ts +16 -0
- package/lib/esm/shapes/filestores/LocalFileStore.js +77 -8
- package/lib/esm/shapes/filestores/LocalFileStore.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +58 -0
- package/lib/esm/utils/queryPlane.js +208 -0
- package/lib/esm/utils/queryPlane.js.map +1 -0
- package/lib/esm/utils/rawQueries.d.ts +24 -0
- package/lib/esm/utils/rawQueries.js +42 -0
- package/lib/esm/utils/rawQueries.js.map +1 -0
- package/lib/esm/utils/rpcExposure.d.ts +53 -0
- package/lib/esm/utils/rpcExposure.js +199 -0
- package/lib/esm/utils/rpcExposure.js.map +1 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @\_linked/server
|
|
2
2
|
|
|
3
|
+
## 2.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#136](https://github.com/linked-fw/server/pull/136) [`0944148`](https://github.com/linked-fw/server/commit/09441482df4c56093a45364b1c899b5f5968c09f) Thanks [@flyon](https://github.com/flyon)! - New server option `server.rawQueries: 'off' | 'session'` (or the `LINKED_RAW_QUERIES` environment variable; the config wins) decides who may run raw SPARQL through `/api/select-raw`. `'session'`, the default, runs it for any signed-in session; `'off'` refuses it. An invalid value turns raw queries off, with a warning. The JSON body parser no longer keeps the unparsed body on `request.rawBody`. Requires `@_linked/server-utils` ^1.10.0.
|
|
8
|
+
|
|
9
|
+
### Behaviour changes
|
|
10
|
+
- `/api/select-raw` now needs a signed-in session (401 without one, in every `rpcExposure` mode), or is refused (403) when `rawQueries` is `'off'`. Before, it was refused unless the app registered a raw query authorizer (`registerRawQueryAuthorizer`, now removed from server-utils), which could also admit a caller without a session. Apps that do not want raw SPARQL reachable should set `rawQueries: 'off'`.
|
|
11
|
+
|
|
12
|
+
## 2.21.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- [#134](https://github.com/linked-fw/server/pull/134) [`6944752`](https://github.com/linked-fw/server/commit/6944752608eafa3ee1112da42ecafb6281684912) Thanks [@flyon](https://github.com/flyon)! - `LocalFileStore` path containment resolves symlinks: the base folder and the target (or its nearest existing ancestor, following a dangling link) go through `fs.realpath` before the comparison, so a key that reaches outside the store through a symlink is refused on every method.
|
|
17
|
+
|
|
18
|
+
`getShapes` on the default provider is not declared callable (it answers 501 in `enforce` mode); clients read the shape index through `/api/all-shapes`. `dispose` is a reserved name: the server calls it when it reloads providers, and no RPC reaches it in either `rpcExposure` mode.
|
|
19
|
+
|
|
20
|
+
- [#134](https://github.com/linked-fw/server/pull/134) [`6944752`](https://github.com/linked-fw/server/commit/6944752608eafa3ee1112da42ecafb6281684912) Thanks [@flyon](https://github.com/flyon)! - RPC exposure rules, per-call request context and generic query plane authorization. Requires `@_linked/server-utils` `^1.9.0`.
|
|
21
|
+
|
|
22
|
+
- `/call/...` dispatches provider methods by declaration. Reserved names always answer 501, also for backend-to-backend calls: `Object.prototype` members, every `BackendProvider`/`ShapeProvider` method (including subclass overrides of lifecycle hooks such as `initRequest`), `dispose`, `constructor`, `__proto__`, accessors and fields.
|
|
23
|
+
- Methods declared with `@callable('public' | 'user')` (from `@_linked/server-utils/utils/callable`) are dispatched; `'user'` answers 401 without a session. An override that does not redeclare a method keeps the strictest level declared for it up the class chain. Methods no class declares run and are logged once per method in `warn` mode, and answer 501 in `enforce` mode. Set the mode with the `server.rpcExposure` option or `LINKED_RPC_EXPOSURE`; the default is `warn`.
|
|
24
|
+
- Methods declared with `@internal()` or `declareInternal(cls, methods)` by the provider's class or a super class answer 501 over HTTP in both modes, even when also declared callable. Backend-to-backend calls still reach them.
|
|
25
|
+
- A provider class with its own `static rpc = false` is not reachable over the generic `/call/<pkg>/<method>` route.
|
|
26
|
+
- Every request runs in one http call context from the first middleware on (routers, provider middleware and error handlers included); backend-to-backend calls outside a request run as system, and lazy provider loading always does. A `{__sc}` Shape-class argument is logged in `warn` and refused (400) in `enforce`.
|
|
27
|
+
- The query endpoints (`selectQuery`/`askQuery`/`createQuery`/`updateQuery`/`deleteQuery` and `/api/select|create|update|delete|select-raw`) apply the `QueryAccess` rules and hand the store the query builder they checked. Before a mutation, one SPARQL `ASK` on the dataset it routes to looks up whether it would write or delete a node typed with a protected class; `setProtectedNodeProbe(fn)` (`utils/queryPlane`) replaces that lookup.
|
|
28
|
+
- `/uploads` and resized images are served with `X-Content-Type-Options: nosniff` and `Content-Security-Policy: sandbox`.
|
|
29
|
+
- `LocalFileStore` refuses keys that resolve outside its base folder.
|
|
30
|
+
- `registerCallRoutes(app, linkedServer)` registers the `/call` and `/api` routes; `mountUploads(app, folder)` and `enterRequestContext` are exported for the same purpose.
|
|
31
|
+
- Client-supplied names in log lines are escaped.
|
|
32
|
+
|
|
33
|
+
Behaviour changes:
|
|
34
|
+
|
|
35
|
+
- Generic-plane mutations require a signed-in user in every mode (401), and a create may not choose the ids of its new nodes (403).
|
|
36
|
+
- With protected shapes registered, a mutation on a dataset that cannot run SPARQL (`rawQuery`) is refused (403), unless the app sets its own probe.
|
|
37
|
+
- `/api/select-raw` is refused (403) in every mode unless the app registers `registerRawQueryAuthorizer`. Its JSON body is also kept as received on `request.rawBody`, which the raw authorizers get as `rawBody` to verify a signature over the exact bytes; with every raw authorizer accepting, the query runs without a session.
|
|
38
|
+
- A query body that is not a query answers 400, as does an operation that does not match the query kind (a delete sent to `selectQuery`).
|
|
39
|
+
- A query that contains a query-context reference (`{"@ctx": name}`, e.g. built with `.for(getQueryContext('user'))`) answers 400 on the query endpoints. A server-side context map is process-wide and does not identify the caller; send the node id instead.
|
|
40
|
+
- `/uploads` files are sandboxed: an HTML file opened from there runs without scripts.
|
|
41
|
+
|
|
3
42
|
## 2.20.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/lib/esm/backend.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
1
10
|
import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';
|
|
11
|
+
import { callable } from '@_linked/server-utils/utils/callable';
|
|
2
12
|
import { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';
|
|
3
13
|
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
4
|
-
import { fromJSON } from '@_linked/core';
|
|
5
14
|
import { LocalFileStore } from './shapes/filestores/LocalFileStore.js';
|
|
6
15
|
import { getShapeIndex } from './utils/Shapes.js';
|
|
16
|
+
import { authorizeGenericQuery, toQueryBuilder } from './utils/queryPlane.js';
|
|
7
17
|
import path from 'path';
|
|
8
18
|
import fs from 'fs/promises';
|
|
9
19
|
export default class LincdServerBackendProvider extends BackendProvider {
|
|
@@ -21,6 +31,9 @@ export default class LincdServerBackendProvider extends BackendProvider {
|
|
|
21
31
|
});
|
|
22
32
|
}
|
|
23
33
|
}
|
|
34
|
+
// Not callable over RPC: no client calls it. Clients read the same index
|
|
35
|
+
// through LincdAPI's `/api/all-shapes` and `/api/shape-details`; backend code
|
|
36
|
+
// can still reach it with a local `Server.call`.
|
|
24
37
|
getShapes() {
|
|
25
38
|
return getShapeIndex();
|
|
26
39
|
}
|
|
@@ -40,20 +53,66 @@ export default class LincdServerBackendProvider extends BackendProvider {
|
|
|
40
53
|
// `toJSON()` and `fromJSON()` rehydrates it here into a live, closed query.
|
|
41
54
|
// `LinkedStorage` then routes it by the QUERY's shape — which is, and always was, what
|
|
42
55
|
// decides the target dataset. The store instance never participated.
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
//
|
|
57
|
+
// Every query is checked against the generic-plane rules first (session,
|
|
58
|
+
// protected shapes and nodes, registered authorizers): see utils/queryPlane.
|
|
59
|
+
// The store runs the builder that was checked. They are declared 'public'
|
|
60
|
+
// because that check, not the RPC layer, decides whether a session is
|
|
61
|
+
// required (always for mutations; for reads it follows `rpcExposure`).
|
|
62
|
+
async selectQuery(json) {
|
|
63
|
+
const query = toQueryBuilder(json);
|
|
64
|
+
await authorizeGenericQuery('select', json, query, 'selectQuery');
|
|
65
|
+
return LinkedStorage.selectQuery(query);
|
|
45
66
|
}
|
|
46
|
-
askQuery(json) {
|
|
47
|
-
|
|
67
|
+
async askQuery(json) {
|
|
68
|
+
const query = toQueryBuilder(json);
|
|
69
|
+
await authorizeGenericQuery('ask', json, query, 'askQuery');
|
|
70
|
+
return LinkedStorage.askQuery(query);
|
|
48
71
|
}
|
|
49
|
-
updateQuery(json) {
|
|
50
|
-
|
|
72
|
+
async updateQuery(json) {
|
|
73
|
+
const query = toQueryBuilder(json);
|
|
74
|
+
await authorizeGenericQuery('update', json, query, 'updateQuery');
|
|
75
|
+
return LinkedStorage.updateQuery(query);
|
|
51
76
|
}
|
|
52
|
-
createQuery(json) {
|
|
53
|
-
|
|
77
|
+
async createQuery(json) {
|
|
78
|
+
const query = toQueryBuilder(json);
|
|
79
|
+
await authorizeGenericQuery('create', json, query, 'createQuery');
|
|
80
|
+
return LinkedStorage.createQuery(query);
|
|
54
81
|
}
|
|
55
|
-
deleteQuery(json) {
|
|
56
|
-
|
|
82
|
+
async deleteQuery(json) {
|
|
83
|
+
const query = toQueryBuilder(json);
|
|
84
|
+
await authorizeGenericQuery('delete', json, query, 'deleteQuery');
|
|
85
|
+
return LinkedStorage.deleteQuery(query);
|
|
57
86
|
}
|
|
58
87
|
}
|
|
88
|
+
__decorate([
|
|
89
|
+
callable('public'),
|
|
90
|
+
__metadata("design:type", Function),
|
|
91
|
+
__metadata("design:paramtypes", [Object]),
|
|
92
|
+
__metadata("design:returntype", Promise)
|
|
93
|
+
], LincdServerBackendProvider.prototype, "selectQuery", null);
|
|
94
|
+
__decorate([
|
|
95
|
+
callable('public'),
|
|
96
|
+
__metadata("design:type", Function),
|
|
97
|
+
__metadata("design:paramtypes", [Object]),
|
|
98
|
+
__metadata("design:returntype", Promise)
|
|
99
|
+
], LincdServerBackendProvider.prototype, "askQuery", null);
|
|
100
|
+
__decorate([
|
|
101
|
+
callable('public'),
|
|
102
|
+
__metadata("design:type", Function),
|
|
103
|
+
__metadata("design:paramtypes", [Object]),
|
|
104
|
+
__metadata("design:returntype", Promise)
|
|
105
|
+
], LincdServerBackendProvider.prototype, "updateQuery", null);
|
|
106
|
+
__decorate([
|
|
107
|
+
callable('public'),
|
|
108
|
+
__metadata("design:type", Function),
|
|
109
|
+
__metadata("design:paramtypes", [Object]),
|
|
110
|
+
__metadata("design:returntype", Promise)
|
|
111
|
+
], LincdServerBackendProvider.prototype, "createQuery", null);
|
|
112
|
+
__decorate([
|
|
113
|
+
callable('public'),
|
|
114
|
+
__metadata("design:type", Function),
|
|
115
|
+
__metadata("design:paramtypes", [Object]),
|
|
116
|
+
__metadata("design:returntype", Promise)
|
|
117
|
+
], LincdServerBackendProvider.prototype, "deleteQuery", null);
|
|
59
118
|
//# sourceMappingURL=backend.js.map
|
package/lib/esm/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';\nimport { callable } from '@_linked/server-utils/utils/callable';\nimport { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { LocalFileStore } from './shapes/filestores/LocalFileStore.js';\nimport { getShapeIndex } from './utils/Shapes.js';\nimport { authorizeGenericQuery, toQueryBuilder } from './utils/queryPlane.js';\nimport path from 'path';\nimport fs from 'fs/promises';\n\nexport default class LincdServerBackendProvider extends BackendProvider {\n async setupBeforeControllers() {\n if (!LinkedFileStorage.getDefaultStore()) {\n LinkedFileStorage.setDefaultStore(\n new LocalFileStore(process.env.NODE_ENV + '-filestore')\n );\n }\n let fileSystemUploadPath = path.join('data', 'filestores');\n try {\n await fs.access(fileSystemUploadPath);\n } catch (err) {\n await fs.mkdir(path.join(fileSystemUploadPath, 'filestores'), {\n recursive: true,\n });\n }\n }\n\n // Not callable over RPC: no client calls it. Clients read the same index\n // through LincdAPI's `/api/all-shapes` and `/api/shape-details`; backend code\n // can still reach it with a local `Server.call`.\n getShapes() {\n return getShapeIndex();\n }\n\n // ── BackendAPIStore query execution ──────────────────────────────────────\n //\n // These answer the frontend `BackendAPIStore`, which calls\n // `Server.call(packageName, {method}, json)` — the PACKAGE form, so the request\n // arrives here on the generic provider rather than through Shape resolution.\n //\n // They used to live on `BackendAPIStoreProvider`, a `ShapeProvider`, whose methods\n // took the store as a first argument and never read it. Reaching that provider meant\n // the transport had to carry a `shapeURI` and an `instanceNode`, and the server had to\n // rebuild the store with `new (providerShapeClass)({id})` — instantiating a Shape only\n // to discard it. Addressing by package removes all of that.\n //\n // A query cannot cross the wire live (core 2.10.0 contract flip), so the client ships\n // `toJSON()` and `fromJSON()` rehydrates it here into a live, closed query.\n // `LinkedStorage` then routes it by the QUERY's shape — which is, and always was, what\n // decides the target dataset. The store instance never participated.\n\n //\n // Every query is checked against the generic-plane rules first (session,\n // protected shapes and nodes, registered authorizers): see utils/queryPlane.\n // The store runs the builder that was checked. They are declared 'public'\n // because that check, not the RPC layer, decides whether a session is\n // required (always for mutations; for reads it follows `rpcExposure`).\n\n @callable('public')\n async selectQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('select', json, query, 'selectQuery');\n return LinkedStorage.selectQuery(query);\n }\n\n @callable('public')\n async askQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('ask', json, query, 'askQuery');\n return LinkedStorage.askQuery(query);\n }\n\n @callable('public')\n async updateQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('update', json, query, 'updateQuery');\n return LinkedStorage.updateQuery(query);\n }\n\n @callable('public')\n async createQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('create', json, query, 'createQuery');\n return LinkedStorage.createQuery(query);\n }\n\n @callable('public')\n async deleteQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('delete', json, query, 'deleteQuery');\n return LinkedStorage.deleteQuery(query);\n }\n}\n"]}
|
|
@@ -12,6 +12,16 @@ import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
|
12
12
|
import { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';
|
|
13
13
|
import { cached } from '@_linked/core/utils/cached';
|
|
14
14
|
import { getShapeIndex } from '../utils/Shapes.js';
|
|
15
|
+
import { authorizeGenericQuery, authorizeRawQuery, toQueryBuilder, } from '../utils/queryPlane.js';
|
|
16
|
+
import { getRpcExposureMode } from '../utils/rpcExposure.js';
|
|
17
|
+
/** An error carrying an HTTP status, such as a `ServerCallError` (401/403). */
|
|
18
|
+
function isStatusError(error) {
|
|
19
|
+
return (!!error &&
|
|
20
|
+
typeof error.status === 'number' &&
|
|
21
|
+
error.status >= 400 &&
|
|
22
|
+
error.status < 600 &&
|
|
23
|
+
error.name === 'ServerCallError');
|
|
24
|
+
}
|
|
15
25
|
const cacheTime = process.env.NODE_ENV === 'development' ? 0 : Infinity;
|
|
16
26
|
// Named explicitly: tsc emits `let X = class X`, and any later esbuild pass over that JS (Vite's
|
|
17
27
|
// SSR `define` replacement runs one on every file mentioning a defined `process.env.*`) renames
|
|
@@ -19,13 +29,17 @@ const cacheTime = process.env.NODE_ENV === 'development' ? 0 : Infinity;
|
|
|
19
29
|
let LincdAPI = class LincdAPI extends Shape {
|
|
20
30
|
async process(request, response, httpMethod) {
|
|
21
31
|
let { method, action } = request.params;
|
|
22
|
-
const body = JSONParser.parseObject(request.body
|
|
32
|
+
const body = JSONParser.parseObject(request.body, {
|
|
33
|
+
shapeClasses: getRpcExposureMode() === 'enforce' ? 'reject' : 'warn',
|
|
34
|
+
});
|
|
23
35
|
//replace - with _ in action names
|
|
24
36
|
let localMethod = httpMethod + '_' + method.replace(/-/g, '_');
|
|
25
37
|
if (typeof this[localMethod] !== 'function') {
|
|
26
38
|
throw new Error('Unknown method: ' + localMethod);
|
|
27
39
|
}
|
|
28
|
-
//
|
|
40
|
+
// The query routes (`post_*`) check access themselves, through
|
|
41
|
+
// utils/queryPlane. The `get_*` routes describe shapes and stay reachable:
|
|
42
|
+
// apps read each other's `/api/all-shapes` server to server.
|
|
29
43
|
try {
|
|
30
44
|
const result = await this[localMethod](body, request, response);
|
|
31
45
|
if (typeof result !== 'undefined' || !response.headersSent) {
|
|
@@ -34,6 +48,10 @@ let LincdAPI = class LincdAPI extends Shape {
|
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
catch (error) {
|
|
51
|
+
if (!response.headersSent && isStatusError(error)) {
|
|
52
|
+
response.status(error.status).json({ error: error.message });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
37
55
|
console.error(`Error while processing ${localMethod}:`, error);
|
|
38
56
|
if (!response.headersSent) {
|
|
39
57
|
const message = error instanceof Error ? error.message : 'Unknown server error';
|
|
@@ -86,19 +104,30 @@ let LincdAPI = class LincdAPI extends Shape {
|
|
|
86
104
|
};
|
|
87
105
|
}, [], cacheTime);
|
|
88
106
|
}
|
|
89
|
-
post_select({ query }) {
|
|
90
|
-
|
|
107
|
+
async post_select({ query }) {
|
|
108
|
+
const builder = toQueryBuilder(query);
|
|
109
|
+
await authorizeGenericQuery('select', query, builder, 'api/select');
|
|
110
|
+
return LinkedStorage.selectQuery(builder);
|
|
91
111
|
}
|
|
92
|
-
post_create({ query }) {
|
|
93
|
-
|
|
112
|
+
async post_create({ query }) {
|
|
113
|
+
const builder = toQueryBuilder(query);
|
|
114
|
+
await authorizeGenericQuery('create', query, builder, 'api/create');
|
|
115
|
+
return LinkedStorage.createQuery(builder);
|
|
94
116
|
}
|
|
95
|
-
post_update({ query }) {
|
|
96
|
-
|
|
117
|
+
async post_update({ query }) {
|
|
118
|
+
const builder = toQueryBuilder(query);
|
|
119
|
+
await authorizeGenericQuery('update', query, builder, 'api/update');
|
|
120
|
+
return LinkedStorage.updateQuery(builder);
|
|
97
121
|
}
|
|
98
|
-
post_delete({ query }) {
|
|
99
|
-
|
|
122
|
+
async post_delete({ query }) {
|
|
123
|
+
const builder = toQueryBuilder(query);
|
|
124
|
+
await authorizeGenericQuery('delete', query, builder, 'api/delete');
|
|
125
|
+
return LinkedStorage.deleteQuery(builder);
|
|
100
126
|
}
|
|
101
|
-
post_select_raw({ query }) {
|
|
127
|
+
async post_select_raw({ query }) {
|
|
128
|
+
// A raw SPARQL string cannot be analysed for the shapes it touches: the
|
|
129
|
+
// `rawQueries` setting decides (a session by default, or off).
|
|
130
|
+
await authorizeRawQuery(query, 'api/select-raw');
|
|
102
131
|
this.checkRawQuerySupport();
|
|
103
132
|
return LinkedStorage.getDefaultDataset().rawQuery(query);
|
|
104
133
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LincdAPI.js","sourceRoot":"","sources":["../../../src/shapes/LincdAPI.ts"],"names":[],"mappings":"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","sourcesContent":["import { Shape } from '@_linked/core/shapes/Shape';\nimport { linkedShape } from '../package.js';\nimport { server } from '../ontologies/server.js';\nimport { JSONParser } from '@_linked/server-utils/utils/JSONParser';\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';\nimport { cached } from '@_linked/core/utils/cached';\nimport { getShapeIndex, ShapeDetails } from '../utils/Shapes.js';\nimport { SparqlDataset as SPARQLStore } from '@_linked/core/sparql/SparqlDataset';\n\nconst cacheTime = process.env.NODE_ENV === 'development' ? 0 : Infinity;\nexport type ShapeSummary = {\n id: string;\n label: string;\n description: string;\n target: { id: string };\n extends?: { id: string };\n numInstances: number;\n};\n// Named explicitly: tsc emits `let X = class X`, and any later esbuild pass over that JS (Vite's\n// SSR `define` replacement runs one on every file mentioning a defined `process.env.*`) renames\n// the inner binding to `X2`, which would otherwise become this shape's IRI.\n@linkedShape({name: 'LincdAPI'})\nexport class LincdAPI extends Shape {\n static targetClass = server.LincdAPI;\n\n async process(\n request,\n response,\n httpMethod: 'get' | 'post' | 'put' | 'delete'\n ) {\n let { method, action } = request.params;\n const body = JSONParser.parseObject<any>(request.body);\n\n //replace - with _ in action names\n let localMethod = httpMethod + '_' + method.replace(/-/g, '_');\n if (typeof this[localMethod] !== 'function') {\n throw new Error('Unknown method: ' + localMethod);\n }\n\n //TODO: access rights, check if the current user is allowed to execute this method\n try {\n const result = await this[localMethod](body, request, response);\n if (typeof result !== 'undefined' || !response.headersSent) {\n const jsonObject = JSONWriter.toJsObject(result);\n response.json(jsonObject);\n }\n } catch (error) {\n console.error(`Error while processing ${localMethod}:`, error);\n if (!response.headersSent) {\n const message =\n error instanceof Error ? error.message : 'Unknown server error';\n response.status(500).json({ error: message });\n }\n }\n }\n\n get_shape_details({\n shapes,\n }: {\n shapes: string[];\n }): Record<string, ShapeDetails> {\n return cached(\n () => {\n const shapeIndex = getShapeIndex();\n const filteredShapeIndex = {};\n for (const shapeId of shapes) {\n if (shapeIndex[shapeId]) {\n filteredShapeIndex[shapeId] = shapeIndex[shapeId];\n }\n }\n return filteredShapeIndex;\n },\n [shapes],\n cacheTime\n );\n }\n\n get_all_shapes(): Promise<{\n shapes: Record<string, ShapeDetails>;\n defaultGraph: string;\n }> {\n return cached(\n async () => {\n const shapeIndex = getShapeIndex();\n\n const typesWithInstances = new Map<string, number>();\n this.checkRawQuerySupport();\n await (LinkedStorage.getDefaultDataset() as unknown as SPARQLStore)\n .rawQuery(\n `SELECT (COUNT(?s) AS ?count) ?type WHERE { ?s a ?type } GROUP BY ?type`\n )\n .then((results) => {\n // rawQuery also types ASK results (a boolean envelope); this is a SELECT.\n if (!results || !('results' in results)) return;\n results.results.bindings.forEach((binding) => {\n if (binding.type && binding.type.value) {\n typesWithInstances.set(\n binding.type.value,\n parseInt(binding.count.value)\n );\n }\n });\n })\n .catch(console.error);\n\n const shapesWithInstances: Record<string, ShapeDetails> = {};\n for (const shapeId in shapeIndex) {\n const shape = shapeIndex[shapeId];\n shapesWithInstances[shapeId] = {\n ...shape,\n numInstances: typesWithInstances.get(shape.targetClass?.id) || 0,\n };\n }\n return {\n shapes: shapesWithInstances,\n defaultGraph: process.env.DATA_ROOT,\n };\n },\n [],\n cacheTime\n );\n }\n\n post_select({ query }) {\n return LinkedStorage.selectQuery(query);\n }\n post_create({ query }) {\n return LinkedStorage.createQuery(query);\n }\n post_update({ query }) {\n return LinkedStorage.updateQuery(query);\n }\n post_delete({ query }) {\n return LinkedStorage.deleteQuery(query);\n }\n\n post_select_raw({ query }) {\n this.checkRawQuerySupport();\n return (LinkedStorage.getDefaultDataset() as unknown as SPARQLStore).rawQuery(\n query\n );\n }\n\n private checkRawQuerySupport() {\n if (!(LinkedStorage.getDefaultDataset() as unknown as SPARQLStore).rawQuery) {\n throw new Error(\n `Default store (${\n Object.getPrototypeOf(LinkedStorage.getDefaultDataset()).constructor\n .name\n }) does not support raw SPARQL queries`\n );\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"LincdAPI.js","sourceRoot":"","sources":["../../../src/shapes/LincdAPI.ts"],"names":[],"mappings":"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","sourcesContent":["import { Shape } from '@_linked/core/shapes/Shape';\nimport { linkedShape } from '../package.js';\nimport { server } from '../ontologies/server.js';\nimport { JSONParser } from '@_linked/server-utils/utils/JSONParser';\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';\nimport { cached } from '@_linked/core/utils/cached';\nimport { getShapeIndex, ShapeDetails } from '../utils/Shapes.js';\nimport { SparqlDataset as SPARQLStore } from '@_linked/core/sparql/SparqlDataset';\nimport {\n authorizeGenericQuery,\n authorizeRawQuery,\n toQueryBuilder,\n} from '../utils/queryPlane.js';\nimport { getRpcExposureMode } from '../utils/rpcExposure.js';\n\n/** An error carrying an HTTP status, such as a `ServerCallError` (401/403). */\nfunction isStatusError(error: any): error is { status: number; message: string } {\n return (\n !!error &&\n typeof error.status === 'number' &&\n error.status >= 400 &&\n error.status < 600 &&\n error.name === 'ServerCallError'\n );\n}\n\nconst cacheTime = process.env.NODE_ENV === 'development' ? 0 : Infinity;\nexport type ShapeSummary = {\n id: string;\n label: string;\n description: string;\n target: { id: string };\n extends?: { id: string };\n numInstances: number;\n};\n// Named explicitly: tsc emits `let X = class X`, and any later esbuild pass over that JS (Vite's\n// SSR `define` replacement runs one on every file mentioning a defined `process.env.*`) renames\n// the inner binding to `X2`, which would otherwise become this shape's IRI.\n@linkedShape({name: 'LincdAPI'})\nexport class LincdAPI extends Shape {\n static targetClass = server.LincdAPI;\n\n async process(\n request,\n response,\n httpMethod: 'get' | 'post' | 'put' | 'delete'\n ) {\n let { method, action } = request.params;\n const body = JSONParser.parseObject<any>(request.body, {\n shapeClasses: getRpcExposureMode() === 'enforce' ? 'reject' : 'warn',\n });\n\n //replace - with _ in action names\n let localMethod = httpMethod + '_' + method.replace(/-/g, '_');\n if (typeof this[localMethod] !== 'function') {\n throw new Error('Unknown method: ' + localMethod);\n }\n\n // The query routes (`post_*`) check access themselves, through\n // utils/queryPlane. The `get_*` routes describe shapes and stay reachable:\n // apps read each other's `/api/all-shapes` server to server.\n try {\n const result = await this[localMethod](body, request, response);\n if (typeof result !== 'undefined' || !response.headersSent) {\n const jsonObject = JSONWriter.toJsObject(result);\n response.json(jsonObject);\n }\n } catch (error) {\n if (!response.headersSent && isStatusError(error)) {\n response.status(error.status).json({ error: error.message });\n return;\n }\n console.error(`Error while processing ${localMethod}:`, error);\n if (!response.headersSent) {\n const message =\n error instanceof Error ? error.message : 'Unknown server error';\n response.status(500).json({ error: message });\n }\n }\n }\n\n get_shape_details({\n shapes,\n }: {\n shapes: string[];\n }): Record<string, ShapeDetails> {\n return cached(\n () => {\n const shapeIndex = getShapeIndex();\n const filteredShapeIndex = {};\n for (const shapeId of shapes) {\n if (shapeIndex[shapeId]) {\n filteredShapeIndex[shapeId] = shapeIndex[shapeId];\n }\n }\n return filteredShapeIndex;\n },\n [shapes],\n cacheTime\n );\n }\n\n get_all_shapes(): Promise<{\n shapes: Record<string, ShapeDetails>;\n defaultGraph: string;\n }> {\n return cached(\n async () => {\n const shapeIndex = getShapeIndex();\n\n const typesWithInstances = new Map<string, number>();\n this.checkRawQuerySupport();\n await (LinkedStorage.getDefaultDataset() as unknown as SPARQLStore)\n .rawQuery(\n `SELECT (COUNT(?s) AS ?count) ?type WHERE { ?s a ?type } GROUP BY ?type`\n )\n .then((results) => {\n // rawQuery also types ASK results (a boolean envelope); this is a SELECT.\n if (!results || !('results' in results)) return;\n results.results.bindings.forEach((binding) => {\n if (binding.type && binding.type.value) {\n typesWithInstances.set(\n binding.type.value,\n parseInt(binding.count.value)\n );\n }\n });\n })\n .catch(console.error);\n\n const shapesWithInstances: Record<string, ShapeDetails> = {};\n for (const shapeId in shapeIndex) {\n const shape = shapeIndex[shapeId];\n shapesWithInstances[shapeId] = {\n ...shape,\n numInstances: typesWithInstances.get(shape.targetClass?.id) || 0,\n };\n }\n return {\n shapes: shapesWithInstances,\n defaultGraph: process.env.DATA_ROOT,\n };\n },\n [],\n cacheTime\n );\n }\n\n async post_select({ query }) {\n const builder = toQueryBuilder(query);\n await authorizeGenericQuery('select', query, builder, 'api/select');\n return LinkedStorage.selectQuery(builder);\n }\n async post_create({ query }) {\n const builder = toQueryBuilder(query);\n await authorizeGenericQuery('create', query, builder, 'api/create');\n return LinkedStorage.createQuery(builder);\n }\n async post_update({ query }) {\n const builder = toQueryBuilder(query);\n await authorizeGenericQuery('update', query, builder, 'api/update');\n return LinkedStorage.updateQuery(builder);\n }\n async post_delete({ query }) {\n const builder = toQueryBuilder(query);\n await authorizeGenericQuery('delete', query, builder, 'api/delete');\n return LinkedStorage.deleteQuery(builder);\n }\n\n async post_select_raw({ query }) {\n // A raw SPARQL string cannot be analysed for the shapes it touches: the\n // `rawQueries` setting decides (a session by default, or off).\n await authorizeRawQuery(query, 'api/select-raw');\n this.checkRawQuerySupport();\n return (LinkedStorage.getDefaultDataset() as unknown as SPARQLStore).rawQuery(\n query\n );\n }\n\n private checkRawQuerySupport() {\n if (!(LinkedStorage.getDefaultDataset() as unknown as SPARQLStore).rawQuery) {\n throw new Error(\n `Default store (${\n Object.getPrototypeOf(LinkedStorage.getDefaultDataset()).constructor\n .name\n }) does not support raw SPARQL queries`\n );\n }\n }\n}\n"]}
|
|
@@ -2,6 +2,26 @@ import { Express as ExpressServer } from 'express';
|
|
|
2
2
|
import { Server as HttpServer } from 'http';
|
|
3
3
|
import type { LinkedConfig } from '@_linked/cli/interfaces';
|
|
4
4
|
import { Shape } from '@_linked/core/shapes/Shape';
|
|
5
|
+
/**
|
|
6
|
+
* Serve the local upload folder at `/uploads` (where `LocalFileStore` files are
|
|
7
|
+
* reachable), with the headers for untrusted files.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mountUploads(app: {
|
|
10
|
+
use: Function;
|
|
11
|
+
}, folder?: string, maxAge?: number): void;
|
|
12
|
+
/**
|
|
13
|
+
* Express middleware: run the rest of the chain in the request's http context.
|
|
14
|
+
* `LinkedServer.start` installs it first, and again right after the JSON body
|
|
15
|
+
* parser (whose stream callbacks do not carry the context).
|
|
16
|
+
*/
|
|
17
|
+
export declare function enterRequestContext(req: any, res: any, next: any): void;
|
|
18
|
+
/**
|
|
19
|
+
* Headers for files users uploaded (`/uploads`, resized copies): the browser
|
|
20
|
+
* must not guess a type from the bytes, and a document served from them runs
|
|
21
|
+
* sandboxed (no scripts, an opaque origin), so an uploaded HTML or SVG file
|
|
22
|
+
* cannot act on the app's origin.
|
|
23
|
+
*/
|
|
24
|
+
export declare function setUntrustedFileHeaders(res: any): void;
|
|
5
25
|
export declare class LinkedServer extends Shape {
|
|
6
26
|
/**
|
|
7
27
|
* indicates that instances of this shape need to have this rdf.type
|
|
@@ -197,13 +217,25 @@ export declare class LinkedServer extends Shape {
|
|
|
197
217
|
*/
|
|
198
218
|
private installSpaFallback;
|
|
199
219
|
onSourceChange(pkg: string): Promise<void>;
|
|
200
|
-
processBackendMethodCall(request: any, response: any): Promise<
|
|
220
|
+
processBackendMethodCall(request: any, response: any): Promise<any>;
|
|
201
221
|
noCache(response: any): void;
|
|
202
222
|
processAPICall(request: any, response: any, method: 'get' | 'post' | 'put' | 'delete'): Promise<void>;
|
|
203
|
-
processShapeMethodCall(request: any, response: any): Promise<
|
|
223
|
+
processShapeMethodCall(request: any, response: any): Promise<any>;
|
|
204
224
|
callShapeMethod(pkg: string, method: string, shapeURI: string, instanceNode: {
|
|
205
225
|
id: string;
|
|
206
|
-
} | null, args: any[], request
|
|
226
|
+
} | null, args: any[], request?: any, response?: any): any;
|
|
227
|
+
/**
|
|
228
|
+
* Refuse a dispatch the exposure rules do not allow (see utils/rpcExposure).
|
|
229
|
+
* Throws a 501 `ServerCallError`, the same answer as for a missing method.
|
|
230
|
+
*
|
|
231
|
+
* - reserved names: always, for HTTP and backend-to-backend calls alike;
|
|
232
|
+
* - HTTP only: an internal method (in every mode), a generic call to a provider whose class sets
|
|
233
|
+
* `static rpc = false`, and an undeclared method in 'enforce' mode
|
|
234
|
+
* (in 'warn' mode it is logged once and runs).
|
|
235
|
+
*/
|
|
236
|
+
private assertDispatchAllowed;
|
|
237
|
+
/** A declared `'user'` method needs a session: 401 without one, in every mode. */
|
|
238
|
+
private assertSessionForLevel;
|
|
207
239
|
handleErrors(fn: any): (req: any, res: any, next: any) => Promise<any>;
|
|
208
240
|
handleErrorsJson(fn: any): (req: any, res: any, next: any) => Promise<any>;
|
|
209
241
|
sendError(res: any, statusCode: number, message: any, logMessage?: string): void;
|
|
@@ -224,11 +256,23 @@ export declare class LinkedServer extends Shape {
|
|
|
224
256
|
*/
|
|
225
257
|
private collectViteSsrCss;
|
|
226
258
|
render(req: any, res: any): Promise<void>;
|
|
259
|
+
private renderPage;
|
|
227
260
|
sendJson(res: any, obj: any): void;
|
|
228
261
|
initRequest(request: any, response: any): Promise<void>;
|
|
229
262
|
getRequestData(request: any, response: any): Promise<{
|
|
230
263
|
requestLD: string;
|
|
231
264
|
requestObject: string;
|
|
232
265
|
}>;
|
|
233
|
-
callBackendMethod(pkg: string, method: string, args: any[], request
|
|
266
|
+
callBackendMethod(pkg: string, method: string, args: any[], request?: any, response?: any): any;
|
|
234
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Register the RPC routes (`/call/...`) and the `LincdAPI` routes (`/api/...`)
|
|
270
|
+
* of `linkedServer` on an express app.
|
|
271
|
+
*
|
|
272
|
+
* `LinkedServer.start()` calls this; tests call it on a bare express app so they
|
|
273
|
+
* drive exactly the routes a running server has.
|
|
274
|
+
*/
|
|
275
|
+
export declare function registerCallRoutes(app: {
|
|
276
|
+
post: Function;
|
|
277
|
+
get: Function;
|
|
278
|
+
}, linkedServer: LinkedServer): void;
|