mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
88c618023a | ||
![]() |
490836962a | ||
![]() |
868275789d | ||
![]() |
221dcc41cd | ||
![]() |
3e8365bb26 | ||
![]() |
4599bf413e | ||
![]() |
e15c59c157 | ||
![]() |
b93aa31c93 | ||
![]() |
3dd8602acc | ||
![]() |
576eefede6 | ||
![]() |
9c44f375f2 | ||
![]() |
73619fda05 | ||
![]() |
ab419674df | ||
![]() |
ed6f2ec8e9 | ||
![]() |
3aa28fa03b | ||
![]() |
e45bce89aa | ||
![]() |
6b8223ba9c | ||
![]() |
46f5fc239e | ||
![]() |
ecd031e69f | ||
![]() |
d1282f6b1a | ||
![]() |
86e8c09e2d | ||
![]() |
5936aceccd | ||
![]() |
1f474a3c8e | ||
![]() |
a0ea743449 | ||
![]() |
a402aa6382 | ||
![]() |
d350c140fd | ||
![]() |
556899dbdb | ||
![]() |
2f10d22c18 | ||
![]() |
cb38613b4c | ||
![]() |
203f80020c | ||
![]() |
f244b288bd | ||
![]() |
386babff42 | ||
![]() |
f59b2c2970 | ||
![]() |
07499631b4 | ||
![]() |
e20efac3ea | ||
![]() |
099897013c | ||
![]() |
f73dfb31c0 | ||
![]() |
2846c711ab | ||
![]() |
5e60000681 | ||
![]() |
d7078ad692 | ||
![]() |
419312ee5f | ||
![]() |
486241f3d4 | ||
![]() |
cac70b1f88 | ||
![]() |
7abca33b67 | ||
![]() |
fa060b86f3 | ||
![]() |
65bf2bd34e | ||
![]() |
3e59aa4b55 | ||
![]() |
5fc4ce8f73 | ||
![]() |
45640a36d6 | ||
![]() |
b381a9c926 | ||
![]() |
331f83d659 | ||
![]() |
73fbe80cff | ||
![]() |
c96b60d4d3 | ||
![]() |
c65096020e | ||
![]() |
c24e6d5a18 | ||
![]() |
5c1553bdda | ||
![]() |
28af181eee |
2
.github/workflows/cth-test.yml
vendored
2
.github/workflows/cth-test.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16.x
|
||||
- name: Check out the project
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
- name: Install dependencies and run build scripts
|
||||
|
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
tags: ${{ steps.meta-main.outputs.tags || steps.meta-version.outputs.tags }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
|
||||
name: Docker meta edge and version tag
|
||||
id: meta-main
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
@ -66,7 +66,7 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and export to docker
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
@ -85,7 +85,7 @@ jobs:
|
||||
done <<< "${{ needs.docker-meta.outputs.tags }}";
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
8
.github/workflows/mkdocs.yml
vendored
8
.github/workflows/mkdocs.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
outputs:
|
||||
major: ${{ steps.tagged_version.outputs.major || steps.current_version.outputs.major }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: actions/checkout@v4.2.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: mkdocs-prep
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: actions/checkout@v4.2.0
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
needs: [mkdocs-prep, mkdocs]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: actions/checkout@v4.2.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
@ -70,7 +70,7 @@ jobs:
|
||||
- name: Generate typedocs
|
||||
run: npm run typedocs
|
||||
- name: Deploy typedocs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
|
21
.github/workflows/npm-test.yml
vendored
21
.github/workflows/npm-test.yml
vendored
@ -7,10 +7,10 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: actions/checkout@v4.2.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
- run: npm ci --ignore-scripts
|
||||
- run: npm run lint
|
||||
|
||||
@ -27,7 +27,8 @@ jobs:
|
||||
- 18.x
|
||||
- '20.0'
|
||||
- 20.x
|
||||
- 21.x
|
||||
- '22.1'
|
||||
- 22.x
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
@ -37,7 +38,7 @@ jobs:
|
||||
- name: Ensure line endings are consistent
|
||||
run: git config --global core.autocrlf input
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Type-check tests
|
||||
@ -59,7 +60,7 @@ jobs:
|
||||
node-version:
|
||||
- 18.x
|
||||
- 20.x
|
||||
- 21.x
|
||||
- 22.x
|
||||
env:
|
||||
TEST_DOCKER: true
|
||||
services:
|
||||
@ -80,7 +81,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Run integration tests
|
||||
@ -94,7 +95,7 @@ jobs:
|
||||
node-version:
|
||||
- 18.x
|
||||
- 20.x
|
||||
- 21.x
|
||||
- 22.x
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
@ -104,7 +105,7 @@ jobs:
|
||||
- name: Ensure line endings are consistent
|
||||
run: git config --global core.autocrlf input
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Run integration tests
|
||||
@ -124,9 +125,9 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Run deploy tests
|
||||
|
@ -22,7 +22,7 @@ module.exports = {
|
||||
// Allow multiple subheadings with the same content
|
||||
// across different section (#1 ##A ##B #2 ##A ##B)
|
||||
MD024: {
|
||||
allow_different_nesting: true,
|
||||
siblings_only: true,
|
||||
},
|
||||
|
||||
// Set Ordered list item prefix to "ordered" (use 1. 2. 3. not 1. 1. 1.)
|
||||
|
49
CHANGELOG.md
49
CHANGELOG.md
@ -3,6 +3,55 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [7.1.2](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.1.1...v7.1.2) (2024-08-20)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Use full encoded topic iri in streaming http receiveFrom url template ([3e8365b](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/3e8365bb2613737fb28c376b5967a351a1300432))
|
||||
|
||||
## [7.1.1](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.1.0...v7.1.1) (2024-08-07)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Ensure streaming HTTP streams the whole notification in a single chunk ([3dd8602](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/3dd8602acce892b36d1ecaf584c938032e754213))
|
||||
|
||||
### Chores
|
||||
|
||||
* Increase jest timeout ([e15c59c](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/e15c59c157882181340fa87a7116b5b34252a79b))
|
||||
* Use correct markdownlint-cli2 fix command ([b93aa31](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/b93aa31c932935c21f1e3666fdab3d0947a645eb))
|
||||
* Depend on external eslint package ([46f5fc2](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/46f5fc239efa794f5309834fa818d17c96f83bd1))
|
||||
|
||||
### Documentation
|
||||
|
||||
* Update server architecture documentation ([9c44f37](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/9c44f375f2537fa0277a6c6831c63c1c1cfc5373))
|
||||
* Explain oidc.json ([73619fd](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/73619fda056d5a9b0b0fac271f29fbced0424169))
|
||||
* Explain WAC vs ACP ([ab41967](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/ab419674df5a92054128588747c3abc06086c3ab))
|
||||
* Explain the provided configs ([ed6f2ec](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/ed6f2ec8e953e84efa6701482d00f616cf6ecbc2))
|
||||
* Add test instructions to documentation ([3aa28fa](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/3aa28fa03b4d1324998e7f6a5ebe5788d0e6b2c9))
|
||||
* Add more explicit installation instructions ([e45bce8](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/e45bce89aabc95b34ecbefcf46f899a88e60cfef))
|
||||
* Add missing index for starting the server ([d350c14](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/d350c140fd184d33cbaf6880b9d4b1476d1ffb7c))
|
||||
* Add HTTP streaming notification option to docs ([556899d](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/556899dbdbf3bb285de71225d156c4891dce23a9))
|
||||
|
||||
## [7.1.0](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.5...v7.1.0) (2024-05-24)
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for StreamingHTTPChannel2023 notifications ([cb38613](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/cb38613b4cea7f4e808b30a69f1d9aecbb9506e2))
|
||||
* Store original target in error metadata ([419312e](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/419312ee5f4790881a5d101afea7ab6ca88f5e61))
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix .nvmrc version ([0749963](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/07499631b44154fd24d3fd8fd704df34dfca0d0a))
|
||||
* Combine metadata with data when generating resources ([e20efac](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/e20efac3eaa79b2ed8b09cd72a7f8f0d85655894))
|
||||
* Make `getParentContainer` work with query parameters ([0998970](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/099897013c4ea014212495965d4972e5078ed406))
|
||||
* Do not reuse the same error in StaticThrowHandler ([f73dfb3](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/f73dfb31c0fe132524323acf6c4f4636bcd8bc80))
|
||||
* Make allow headers more accurate ([5e60000](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/5e600006819ae1cf1f8edf804218aee700c59bae))
|
||||
* Expose auxiliary links on errors ([d7078ad](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/d7078ad69261566c44e38d1bb19142fb8bd4dd0f))
|
||||
|
||||
### Refactors
|
||||
|
||||
* Simplify eslint configs ([cac70b1](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/cac70b1f88dcbbb3ebbe0b8e0b082ead4ab27b33))
|
||||
|
||||
## [7.0.5](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.4...v7.0.5) (2024-03-25)
|
||||
|
||||
### Fixes
|
||||
|
@ -31,7 +31,9 @@ And, of course, for many others who like to experience Solid.
|
||||
|
||||
## ⚡ Running the Community Solid Server
|
||||
|
||||
Use [Node.js](https://nodejs.org/en/) 18.0 or up and execute:
|
||||
Make sure you have [Node.js](https://nodejs.org/en/) 18.0 or higher.
|
||||
If this is your first time using Node.js,
|
||||
you can find instructions on how to do this [here](https://nodejs.org/en/download/package-manager).
|
||||
|
||||
```shell
|
||||
npx @solid/community-server
|
||||
@ -45,7 +47,9 @@ To persist your pod's contents between restarts, use:
|
||||
npx @solid/community-server -c @css:config/file.json -f data/
|
||||
```
|
||||
|
||||
Find more ways to run the server in the [documentation](https://communitysolidserver.github.io/CommunitySolidServer/latest/usage/starting-server/).
|
||||
In case you prefer to use Docker instead,
|
||||
you can find instructions for this and other methods in the
|
||||
[documentation](https://communitysolidserver.github.io/CommunitySolidServer/latest/usage/starting-server/).
|
||||
|
||||
## 🔧 Configure your server
|
||||
|
||||
|
152
config/README.md
152
config/README.md
@ -1,67 +1,129 @@
|
||||
# Configuration
|
||||
|
||||
This folder contains several configurations that can be used to start up the server.
|
||||
These can be used directly, or used as inspiration on how you would want to configure your server.
|
||||
All those configurations are created in the same way:
|
||||
features are enabled or disabled by choosing a specific option for every component.
|
||||
All components are represented by the subfolders found in the folders here:
|
||||
`ldp` contains all LDP related components,
|
||||
`identity` all IDP components, etc.
|
||||
Options are then chosen by importing 1 entry from every component subfolder.
|
||||
In case none of the available options have the exact feature configuration you want,
|
||||
it is always possible to not choose any of them and create your own custom version instead.
|
||||
More information on how this can be done manually,
|
||||
can be found in this [tutorial](https://github.com/CommunitySolidServer/tutorials/blob/main/custom-configurations.md).
|
||||
|
||||
## How to use
|
||||
As manually changing server options can be cumbersome,
|
||||
there is also an online [configuration generator](https://communitysolidserver.github.io/configuration-generator/).
|
||||
|
||||
The easiest way to create a new config is by creating a JSON-LD file
|
||||
that imports one option from every component subfolder
|
||||
(such as either `allow-all.json` or `webacl.json` from `ldp/authorization`).
|
||||
In case none of the available options suffice, there are 2 other ways to handle this:
|
||||
Below we give an overview of the main identifying features of the configurations.
|
||||
We start with all features of the default configuration,
|
||||
after which we will explain in which features the other ones differ from it.
|
||||
|
||||
### Append to an existing config
|
||||
## default.json
|
||||
|
||||
In case the options mostly suffice, but they just need to do a bit more,
|
||||
it might be possible to append to one of the solutions.
|
||||
This is the configuration that is used if no configuration is provided when starting the server.
|
||||
It stores all data in memory, so this server is perfect for quickly trying some things out,
|
||||
but not if you want a persistent server.
|
||||
|
||||
For example, in case all the existing metadata parsers can remain,
|
||||
but an additional one needs to be added,
|
||||
you could import `ldp/metadata-parser/default.json`
|
||||
and then add the following in your root config:
|
||||
For authorization, it uses [Web Access Control (WAC)](https://solid.github.io/web-access-control-spec/),
|
||||
it supports all [notification methods](https://solidproject.org/TR/notifications-protocol) implemented in CSS,
|
||||
allows users to create accounts, pods, WebIDs, and use them for [Solid-OIDC](https://solid.github.io/solid-oidc/).
|
||||
|
||||
```json
|
||||
{
|
||||
"@id": "urn:solid-server:default:MetadataParser",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{ "@type": "MyNewParser" }
|
||||
]
|
||||
}
|
||||
```
|
||||
It is also initialized with an `index.html` page at root level,
|
||||
with permissions set in such a way that everyone has full access to the server.
|
||||
|
||||
This will add the new parser to the list of metadata parsers.
|
||||
The `@id` value is needed so Components.js knows which object to add the values to,
|
||||
and the `@type` is needed so it can interpret the other fields (`handlers` in this case).
|
||||
Although strictly not allowed by the Solid specification,
|
||||
this configuration allows users to both write data at root level of the server,
|
||||
and also create pods in subcontainers.
|
||||
In all other configurations only or the other (or neither) will be allowed,
|
||||
but here both are enabled for maximum flexibility when testing things out.
|
||||
|
||||
Note that generally it is only advised to append to ParallelHandlers or key/value maps.
|
||||
In case the order is important this can not be guaranteed over separate files.
|
||||
## file.json
|
||||
|
||||
### Create a new option
|
||||
The most important difference with the `default.json` configuration is that this one stores its data as files on disk,
|
||||
thereby making the data persistent.
|
||||
Besides that, it also prevents data from being written to the root,
|
||||
the only way to add data is to create a pod and add data there.
|
||||
To still show something at root level when the server is started,
|
||||
a static page is shown which can not be modified using standard Solid requests.
|
||||
|
||||
If a more drastic change is required,
|
||||
the solution is to not import anything from that folder but instead write your own.
|
||||
## file-acp.json
|
||||
|
||||
For example, in case you only want the slug parser but not any of the others,
|
||||
you would have to not import anything from `ldp/metadata-parser` folder,
|
||||
but instead have the following in your root config:
|
||||
The only difference with `file.json`is that this uses
|
||||
[Access Control Policy (ACP)](https://solid.github.io/authorization-panel/acp-specification/)
|
||||
for authorization instead of WAC.
|
||||
|
||||
```json
|
||||
{
|
||||
"@id": "urn:solid-server:default:MetadataParser",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{ "@type": "SlugParser" }
|
||||
]
|
||||
}
|
||||
```
|
||||
## file-root.json
|
||||
|
||||
Don't forget that in some cases you would also have to copy some imports!
|
||||
The existing options can be used as inspiration.
|
||||
This configuration starts from `file.json`, but does not allow the creation of accounts.
|
||||
Instead, it allows data to be written directly to the root of the server.
|
||||
To make sure users can write data there after starting the server,
|
||||
permissions have been set to grant everyone full access,
|
||||
so this needs to be changed after starting the server.
|
||||
|
||||
## file-root-pod.json
|
||||
|
||||
The same idea as `file-root.json`,
|
||||
but here it is done by creating an account with a pod
|
||||
in the root of the server the first time it is started.
|
||||
The credentials to this account are stored in the configuration so should be changed afterwards.
|
||||
This has the advantage of both having your data at root level,
|
||||
but also allowing you to authenticate using Solid-OIDC.
|
||||
|
||||
## https-file-cli.json
|
||||
|
||||
A variant of `file.json` that uses HTTPS of HTTP.
|
||||
The required key and cert file paths need to be defined using two new CLI options: `--httpsKey` and `-httpCert`.
|
||||
|
||||
## example-https-file.json
|
||||
|
||||
Another way to define HTTPS, but this time through the configuration file itself instead of the CLI.
|
||||
As can be seen in the configuration itself, two paths are defined, pointing to the key and cert files.
|
||||
To actually use this solution, you need to update the paths in that file before running the server.
|
||||
|
||||
## sparql-endpoint.json
|
||||
|
||||
Sets up a server that uses a SPARQL endpoint to store the data.
|
||||
Only RDF data can be stored on a server using this configuration.
|
||||
For internal data, such as accounts, temporary OIDC resources, etc,
|
||||
the servers uses non-RDF data formats.
|
||||
While other configurations store this kind of data in the same backend as the Solid data,
|
||||
this is not feasible when using a SPARQL endpoint.
|
||||
For this reason, this configuration stores all that data in memory,
|
||||
meaning this solution should not be used if you want persistent accounts.
|
||||
|
||||
## sparql-endpoint-root.json
|
||||
|
||||
This differs from `sparql-endpoint.json` in the same way as `file-root.json` differs from `file.json`.
|
||||
|
||||
## sparql-file-storage.json
|
||||
|
||||
Similar to `sparql-endpoint.json` with the main difference being
|
||||
that here internal data is stored on disk instead of in memory.
|
||||
|
||||
## memory-subdomains.json
|
||||
|
||||
A memory-based server whose main differentiating feature is how pod URLs are constructed.
|
||||
In most other configurations, pods are created by appending the chosen name to the base URL of the server,
|
||||
so for a server running at `http://example.com/`,
|
||||
choosing the name `test` for your pod would result in `http://example.com/test/`.
|
||||
With this configuration, the name is used as a subdomain of the url instead,
|
||||
so the above values would result in a pod at `http://test.example.com/` instead.
|
||||
|
||||
## quota-file.json
|
||||
|
||||
A file-based server that limits the amount of data a user can put in a pod.
|
||||
The values in the configuration determine the limit.
|
||||
|
||||
## path-routing.json
|
||||
|
||||
This configuration serves as an example of how a server can be configured
|
||||
to serve data from different backends depending on the URL that is used.
|
||||
In this example, all data in the `/sparql/` container will be stored in a SPARQL backend,
|
||||
and similarly for `/memory/` and `/file/`.
|
||||
|
||||
## oidc.json
|
||||
|
||||
A configuration that sets up the server to only function as an Identity Provider.
|
||||
It does not support creating pods or storing data on the server,
|
||||
the only available options are creating accounts and linking them to WebIDs.
|
||||
This way the server can be used to identify those WebIDs during an OIDC interaction.
|
||||
|
@ -22,12 +22,14 @@ Determines how notifications should be sent out from the server when resources c
|
||||
|
||||
* *all*: Supports all available notification types of the Solid Notifications protocol
|
||||
[specification](https://solidproject.org/TR/notifications-protocol).
|
||||
Currently, this includes WebhookChannel2023 and WebSocketChannel2023.
|
||||
Currently, this includes WebhookChannel2023, WebSocketChannel2023 and StreamingHTTPChannel2023.
|
||||
* *disabled*: No notifications are sent out.
|
||||
* *legacy-websocket*: Follows the legacy Solid WebSocket
|
||||
[specification](https://github.com/solid/solid-spec/blob/master/api-websockets.md).
|
||||
Will be removed in future versions.
|
||||
* *new-old-websockets.json*: Support for both the legacy Solid Websockets and the new WebSocketChannel2023.
|
||||
* *streaming-http*: Follows the StreamingHTTPChannel2023
|
||||
[specification](https://solid.github.io/notifications/streaming-http-channel-2023) draft.
|
||||
* *webhooks*: Follows the WebhookChannel2023
|
||||
[specification](https://solid.github.io/notifications/webhook-channel-2023) draft.
|
||||
* *websockets*: Follows the WebSocketChannel2023
|
||||
|
@ -6,6 +6,7 @@
|
||||
"css:config/http/notifications/base/http.json",
|
||||
"css:config/http/notifications/base/listener.json",
|
||||
"css:config/http/notifications/base/storage.json",
|
||||
"css:config/http/notifications/streaming-http/http.json",
|
||||
"css:config/http/notifications/websockets/handler.json",
|
||||
"css:config/http/notifications/websockets/http.json",
|
||||
"css:config/http/notifications/websockets/subscription.json",
|
||||
|
15
config/http/notifications/streaming-http.json
Normal file
15
config/http/notifications/streaming-http.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/http/notifications/base/description.json",
|
||||
"css:config/http/notifications/base/handler.json",
|
||||
"css:config/http/notifications/base/http.json",
|
||||
"css:config/http/notifications/base/storage.json",
|
||||
"css:config/http/notifications/streaming-http/http.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "All the relevant components are made in the specific imports seen above."
|
||||
}
|
||||
]
|
||||
}
|
87
config/http/notifications/streaming-http/http.json
Normal file
87
config/http/notifications/streaming-http/http.json
Normal file
@ -0,0 +1,87 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:default:StreamingHTTP2023Route",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:default:NotificationRoute" },
|
||||
"relativePath": "/StreamingHTTPChannel2023/"
|
||||
},
|
||||
{
|
||||
"comment": "Creates updatesViaStreamingHttp2023 Link relations",
|
||||
"@id": "urn:solid-server:default:StreamingHttpMetadataWriter",
|
||||
"@type": "StreamingHttpMetadataWriter",
|
||||
"route": { "@id": "urn:solid-server:default:StreamingHTTP2023Route" }
|
||||
},
|
||||
{
|
||||
"comment": "Allows discovery of the corresponding streaming HTTP channel",
|
||||
"@id": "urn:solid-server:default:MetadataWriter",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:StreamingHttpMetadataWriter" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "Handles the request targeting a StreamingHTTPChannel2023 receiveFrom endpoint.",
|
||||
"@id": "urn:solid-server:default:StreamingHttp2023Router",
|
||||
"@type": "OperationRouterHandler",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"allowedMethods": [ "GET" ],
|
||||
"allowedPathNames": [ "/StreamingHTTPChannel2023/" ],
|
||||
"handler": {
|
||||
"@id": "urn:solid-server:default:StreamingHttp2023RequestHandler",
|
||||
"@type": "StreamingHttpRequestHandler",
|
||||
"streamMap": { "@id": "urn:solid-server:default:StreamingHttpMap" },
|
||||
"route": { "@id": "urn:solid-server:default:StreamingHTTP2023Route" },
|
||||
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
||||
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
||||
"credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
||||
"permissionReader": { "@id": "urn:solid-server:default:PermissionReader" },
|
||||
"authorizer": { "@id": "urn:solid-server:default:Authorizer" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "Add the router to notification type handler",
|
||||
"@id": "urn:solid-server:default:NotificationTypeHandler",
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:StreamingHttp2023Router" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "Opened response streams will be stored in this Map.",
|
||||
"@id": "urn:solid-server:default:StreamingHttpMap",
|
||||
"@type": "StreamingHttpMap"
|
||||
},
|
||||
{
|
||||
"comment": "Emits serialized notifications through Streaming HTTP.",
|
||||
"@id": "urn:solid-server:default:StreamingHttp2023Emitter",
|
||||
"@type": "StreamingHttp2023Emitter",
|
||||
"streamMap": { "@id": "urn:solid-server:default:StreamingHttpMap" }
|
||||
},
|
||||
{
|
||||
"comment": "Listens to the activities emitted by the MonitoringStore.",
|
||||
"@id": "urn:solid-server:default:StreamingHttpListeningActivityHandler",
|
||||
"@type": "StreamingHttpListeningActivityHandler",
|
||||
"emitter": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||
"streamMap": { "@id": "urn:solid-server:default:StreamingHttpMap" },
|
||||
"source": {
|
||||
"comment": "Handles the generation and serialization of notifications for StreamingHTTPChannel2023",
|
||||
"@id": "urn:solid-server:default:StreamingHttpNotificationHandler",
|
||||
"@type": "ComposedNotificationHandler",
|
||||
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
||||
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
||||
"emitter": { "@id": "urn:solid-server:default:StreamingHttp2023Emitter" },
|
||||
"eTagHandler": { "@id": "urn:solid-server:default:ETagHandler" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "Add the activity handler to the primary initializer",
|
||||
"@id": "urn:solid-server:default:PrimaryParallelInitializer",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:StreamingHttpListeningActivityHandler" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -39,9 +39,16 @@
|
||||
},
|
||||
"enabledJWA": {
|
||||
"dPoPSigningAlgValues": [
|
||||
"RS256", "RS384", "RS512",
|
||||
"PS256", "PS384", "PS512",
|
||||
"ES256", "ES256K", "ES384", "ES512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES256K",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"EdDSA"
|
||||
]
|
||||
},
|
||||
|
@ -7,20 +7,26 @@
|
||||
"@type": "SafeErrorHandler",
|
||||
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
|
||||
"errorHandler": {
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{
|
||||
"comment": "Internally redirects are created by throwing a specific error, this handler converts them to the correct response.",
|
||||
"@type": "RedirectingErrorHandler"
|
||||
},
|
||||
{
|
||||
"comment": "Converts an Error object into a representation for an HTTP response.",
|
||||
"@type": "ConvertingErrorHandler",
|
||||
"converter": { "@id": "urn:solid-server:default:UiEnabledConverter" },
|
||||
"preferenceParser": { "@id": "urn:solid-server:default:PreferenceParser" },
|
||||
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
}
|
||||
]
|
||||
"@id": "urn:solid-server:default:TargetExtractorErrorHandler",
|
||||
"@type": "TargetExtractorErrorHandler",
|
||||
"targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||
"errorHandler": {
|
||||
"@id": "urn:solid-server:default:WaterfallErrorHandler",
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{
|
||||
"comment": "Redirects are created internally by throwing a specific error; this handler converts them to the correct response.",
|
||||
"@type": "RedirectingErrorHandler"
|
||||
},
|
||||
{
|
||||
"comment": "Converts an Error object into a representation for an HTTP response.",
|
||||
"@type": "ConvertingErrorHandler",
|
||||
"converter": { "@id": "urn:solid-server:default:UiEnabledConverter" },
|
||||
"preferenceParser": { "@id": "urn:solid-server:default:PreferenceParser" },
|
||||
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -38,6 +38,7 @@ the [changelog](https://github.com/CommunitySolidServer/CommunitySolidServer/blo
|
||||
* [How to automatically seed pods on startup](usage/seeding-pods.md)
|
||||
* [Receiving notifications when resources change](usage/notifications.md)
|
||||
* [Using the CSS as a development server in another project](usage/dev-configuration.md)
|
||||
* [Which authorization method to pick](usage/authorization-methods.md)
|
||||
|
||||
## What the internals look like
|
||||
|
||||
|
@ -17,7 +17,7 @@ flowchart LR
|
||||
ControlHandler --password--> PasswordControlHandler("<strong>PasswordControlHandler</strong><br>ControlHandler")
|
||||
ControlHandler --"oidc"--> OidcControlHandler("<strong>OidcControlHandler</strong><br>OidcControlHandler")
|
||||
ControlHandler --html--> HtmlControlHandler("<strong>HtmlControlHandler</strong><br>ControlHandler")
|
||||
|
||||
|
||||
HtmlControlHandler --main--> MainHtmlControlHandler("<strong>MainHtmlControlHandler</strong><br>ControlHandler")
|
||||
HtmlControlHandler --account--> AccountHtmlControlHandler("<strong>AccountHtmlControlHandler</strong><br>ControlHandler")
|
||||
HtmlControlHandler --password--> PasswordHtmlControlHandler("<strong>PasswordHtmlControlHandler</strong><br>ControlHandler")
|
||||
|
@ -10,7 +10,7 @@ flowchart LR
|
||||
Handler("<strong>IdentityProviderHandler</strong><br>RouterHandler")
|
||||
ParsingHandler("<strong>IdentityProviderParsingHandler</strong><br>AuthorizingHttpHandler")
|
||||
AuthorizingHandler("<strong>IdentityProviderAuthorizingHandler</strong><br>AuthorizingHttpHandler")
|
||||
|
||||
|
||||
Handler --> ParsingHandler
|
||||
ParsingHandler --> AuthorizingHandler
|
||||
AuthorizingHandler --> HttpHandler("<strong>IdentityProviderHttpHandler</strong><br>IdentityProviderHttpHandler")
|
||||
@ -26,12 +26,12 @@ flowchart TD
|
||||
HttpHandler("<strong>IdentityProviderHttpHandler</strong><br>IdentityProviderHttpHandler")
|
||||
HttpHandler --> InteractionHandler("<strong>InteractionHandler</strong><br>WaterfallHandler")
|
||||
InteractionHandler --> InteractionHandlerArgs
|
||||
|
||||
|
||||
subgraph InteractionHandlerArgs[" "]
|
||||
HtmlViewHandler("<strong>HtmlViewHandler</strong><br>HtmlViewHandler")
|
||||
LockingInteractionHandler("<strong>LockingInteractionHandler</strong><br>LockingInteractionHandler")
|
||||
end
|
||||
|
||||
|
||||
LockingInteractionHandler --> JsonConversionHandler("<strong>JsonConversionHandler</strong><br>JsonConversionHandler")
|
||||
JsonConversionHandler --> VersionHandler("<strong>VersionHandler</strong><br>VersionHandler")
|
||||
VersionHandler --> CookieInteractionHandler("<strong>CookieInteractionHandler</strong><br>CookieInteractionHandler")
|
||||
|
@ -108,32 +108,33 @@ to add any custom initializers that need to run.
|
||||
|
||||
The `ServerInitializer` is the initializer that finally starts up the server by listening to the relevant port,
|
||||
once all the initialization described above is finished.
|
||||
It takes as input 2 components: a `HttpServerFactory` and a `ServerListener`.
|
||||
To do this it makes use of an `HttpServerFactory`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ServerInitializer("<strong>ServerInitializer</strong><br>ServerInitializer")
|
||||
ServerInitializer --> ServerInitializerArgs
|
||||
|
||||
subgraph ServerInitializerArgs[" "]
|
||||
ServerInitializer --> ServerFactory("<strong>ServerFactory</strong><br>BaseServerFactory")
|
||||
ServerFactory --> ServerConfigurator("<strong>ServerConfigurator</strong><br>ParallelHandler")
|
||||
ServerConfigurator --> ServerConfiguratorArgs
|
||||
|
||||
subgraph ServerConfiguratorArgs[" "]
|
||||
direction LR
|
||||
ServerFactory("<strong>ServerFactory</strong><br>BaseServerFactory")
|
||||
ServerListener("<strong>ServerListener</strong><br>ParallelHandler")
|
||||
HandlerServerConfigurator("<strong>HandlerServerConfigurator</strong><br>HandlerServerConfigurator")
|
||||
WebSocketServerConfigurator("<strong>WebSocketServerConfigurator</strong><br>WebSocketServerConfigurator")
|
||||
end
|
||||
|
||||
ServerListener --> HandlerServerListener("<strong>HandlerServerListener</strong><br>HandlerServerListener")
|
||||
|
||||
HandlerServerListener --> HttpHandler("<strong>HttpHandler</strong><br><i>HttpHandler</i>")
|
||||
|
||||
HandlerServerConfigurator --> HttpHandler("<strong>HttpHandler</strong><br><i>HttpHandler</i>")
|
||||
WebSocketServerConfigurator --> WebSocketHandler("<strong>WebSocketHandler</strong><br><i>WebSocketHandler</i>")
|
||||
```
|
||||
|
||||
The `HttpServerFactory` is responsible for starting a server on a given port.
|
||||
Depending on the configuration this can be an HTTP or an HTTPS server.
|
||||
The created server emits events when it receives requests.
|
||||
|
||||
A `ServerListener` is a class that takes the created server as input and attaches a listener to interpret events.
|
||||
One listener that is always used is the `urn:solid-server:default:HandlerServerListener`,
|
||||
which calls an `HttpHandler` [to resolve HTTP requests](http-handler.md).
|
||||
|
||||
Sometimes it is necessary to add additional listeners,
|
||||
these can then be added to the `urn:solid-server:default:ServerListener` as it is a `ParallellHandler`.
|
||||
An example of this is when WebSockets are used [to handle notifications](notifications.md).
|
||||
Any requests it receives, it sends to its `ServerConfigurator`,
|
||||
which handles the request as needed.
|
||||
This is a `ParallelHandler`, supporting two kinds of requests:
|
||||
HTTP requests go through a configurator that sends those
|
||||
to an `HttpHandler` [to resolve HTTP requests](http-handler.md).
|
||||
In case WebSockets are enabled [to handle notifications](notifications.md),
|
||||
these are handled by the `WebSocketHandler`.
|
||||
|
@ -78,7 +78,7 @@ flowchart TB
|
||||
ResourceStore("<strong>ResourceStore</strong><br><i>ActivityEmitter</i>")
|
||||
NotificationHandler("<strong>NotificationHandler</strong><br>WaterfallHandler")
|
||||
end
|
||||
|
||||
|
||||
NotificationHandler --> NotificationHandlerArgs
|
||||
subgraph NotificationHandlerArgs[" "]
|
||||
direction TB
|
||||
@ -154,9 +154,9 @@ flowchart TB
|
||||
NotificationChannelStorage("<strong>NotificationChannelStorage</strong><br>NotificationChannelStorage")
|
||||
SequenceHandler("<br>SequenceHandler")
|
||||
end
|
||||
|
||||
|
||||
SequenceHandler --> SequenceHandlerArgs
|
||||
|
||||
|
||||
subgraph SequenceHandlerArgs[" "]
|
||||
direction TB
|
||||
WebSocket2023Storer("<strong>WebSocket2023Storer</strong><br>WebSocket2023Storer")
|
||||
@ -184,3 +184,45 @@ are quite similar to those needed for WebSocketChannel2023:
|
||||
* The `WebhookChannel2023Type` class contains all the necessary typing information.
|
||||
* `WebhookEmitter` is the `NotificationEmitter` that sends the request.
|
||||
* `WebhookUnsubscriber` and `WebhookWebId` are additional utility classes to support the spec requirements.
|
||||
|
||||
## StreamingHTTPChannel2023
|
||||
|
||||
Currently, support for [StreamingHTTPChannel2023](https://solid.github.io/notifications/streaming-http-channel-2023)
|
||||
only covers default, pre-established channels made available for every resource. Those channels output `text/turtle`.
|
||||
|
||||
Support for custom, subscription-based channels can be added in the future.
|
||||
|
||||
* For discovery, there is a `StreamingHttpMetadataWriter`, which adds `Link` to every `HTTP` response header
|
||||
using `rel="http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023"`. It links directly to the `receiveFrom`
|
||||
endpoint of the default, pre-established channel for that topic resource.
|
||||
* Requests to `receiveFrom` endpoints are handled by a `StreamingHttpRequestHandler`.
|
||||
* It performs an authorization check.
|
||||
* It creates a new response stream and adds it to the `StreamingHttpMap`, indexed by the topic resource.
|
||||
* It sends an initial notification, similar to notification channels using a `state` feature.
|
||||
* `StreamingHttp2023Emitter` is the `NotificationEmitter` that writes notifications to matching response streams.
|
||||
* `StreamingHttpListeningActivityHandler` is responsible for observing the `MonitoringStore`
|
||||
and emitting notifications when needed.
|
||||
It doesn't use a `NotificationChannelStorage` since the default, pre-established channels are not
|
||||
subscription-based. Instead, it uses a `StreamingHttpMap` to check for active receivers.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
StreamingHttpListeningActivityHandler("<strong>StreamingHttpListeningActivityHandler</strong><br>StreamingHttpListeningActivityHandler")
|
||||
StreamingHttpListeningActivityHandler --> StreamingHttpListeningActivityHandlerArgs
|
||||
|
||||
subgraph StreamingHttpListeningActivityHandlerArgs[" "]
|
||||
StreamingHttpMap("<strong>StreamingHttpMap</strong><br><i>StreamingHttpMap</i>")
|
||||
ResourceStore("<strong>ResourceStore</strong><br><i>ActivityEmitter</i>")
|
||||
StreamingHttpNotificationHandler("<strong>StreamingHttpNotificationHandler</strong><br><i>ComposedNotificationHandler</i>")
|
||||
end
|
||||
|
||||
StreamingHttpNotificationHandler --> StreamingHttpNotificationHandlerArgs
|
||||
subgraph StreamingHttpNotificationHandlerArgs[" "]
|
||||
direction TB
|
||||
Generator("<strong>BaseNotificationGenerator</strong>")
|
||||
Serializer("<strong>BaseNotificationSerializer</strong>")
|
||||
Emitter("<strong>StreamingHttp2023Emitter</strong><br><i>StreamingHttp2023Emitter</i>")
|
||||
ETagHandler("<strong>ETagHandler</strong>")
|
||||
|
||||
end
|
||||
```
|
||||
|
@ -27,15 +27,15 @@ flowchart LR
|
||||
RdfPatcher("<strong>RdfPatcher</strong><br>RdfPatcher")
|
||||
RdfPatcher --> RDFStore("<strong>PatchHandler_RDFStore</strong><br>WaterfallHandler")
|
||||
RDFStore --> RDFStoreArgs
|
||||
|
||||
|
||||
subgraph RDFStoreArgs[" "]
|
||||
Immutable("<strong>PatchHandler_ImmutableMetadata</strong><br>ImmutableMetadataPatcher")
|
||||
RDF("<strong>PatchHandler_RDF</strong><br>WaterfallHandler")
|
||||
Immutable --> RDF
|
||||
end
|
||||
|
||||
|
||||
RDF --> RDFArgs
|
||||
|
||||
|
||||
subgraph RDFArgs[" "]
|
||||
direction LR
|
||||
N3("<br>N3Patcher")
|
||||
|
76
documentation/markdown/features/test.md
Normal file
76
documentation/markdown/features/test.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Testing the server
|
||||
|
||||
There are several test sets in place to ensure the server conforms to the necessary requirements,
|
||||
and to prevent changes from breaking this.
|
||||
|
||||
## Unit tests
|
||||
|
||||
For every TypeScript file,
|
||||
most of which correspond to a single class implementation,
|
||||
there is a corresponding unit test file in the `test/unit` folder.
|
||||
These tests require 100% code coverage over the corresponding implementation,
|
||||
making sure every line is checked.
|
||||
|
||||
These tests can be run using the `npm run test:unit` script.
|
||||
|
||||
## Integration tests
|
||||
|
||||
The `test/integration` folder contains several test suites that set up a complete server instance
|
||||
and validate its functionality.
|
||||
`test/intergration/config` contains the configurations used by these test suites.
|
||||
These make sure that no features get lost after changes are made to the server.
|
||||
|
||||
These tests can be run using the `npm run test:integration` script.
|
||||
|
||||
## Specification conformance
|
||||
|
||||
To make sure the server conforms to the Solid specification,
|
||||
we run the [Conformance Test Harness (CTH)](https://github.com/solid-contrib/conformance-test-harness)
|
||||
combined with the [specification test suite](https://github.com/solid-contrib/specification-tests/).
|
||||
This test suite was made specifically so any Solid server can be tested
|
||||
on how well it conforms to the Solid specifications.
|
||||
The configuration that runs these tests in the repository can be found [here](https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/.github/workflows/cth-test.yml).
|
||||
|
||||
You can also run this test suite locally.
|
||||
Besides the standard requirements for running the server,
|
||||
this also requires Docker.
|
||||
First make sure you have a running CSS instance,
|
||||
in this example we will assume it is running at `http://localhost:3000`.
|
||||
After that you can run the following commands.
|
||||
The paths are relative to the root folder of your CSS source folder,
|
||||
and should be adjusted accordingly if you are not running this from the source folder.
|
||||
|
||||
```bash
|
||||
# Generate the folder where the reports will be located
|
||||
mkdir -p ../conformance/reports/css
|
||||
|
||||
# Pull the CTH Docker image
|
||||
docker pull solidproject/conformance-test-harness
|
||||
|
||||
# Set up the env file necessary for the CTH
|
||||
echo 'SOLID_IDENTITY_PROVIDER=http://localhost:3000/idp/
|
||||
USERS_ALICE_WEBID=http://localhost:3000/alice/profile/card#me
|
||||
USERS_BOB_WEBID=http://localhost:3000/bob/profile/card#me
|
||||
RESOURCE_SERVER_ROOT=http://localhost:3000
|
||||
TEST_CONTAINER=/alice/
|
||||
quarkus.log.category."ResultLogger".level=INFO
|
||||
quarkus.log.category."com.intuit.karate".level=DEBUG
|
||||
quarkus.log.category."org.solid.testharness.http.Client".level=DEBUG
|
||||
quarkus.log.category."org.solid.testharness.http.AuthManager".level=DEBUG
|
||||
MAXTHREADS=1' > ../conformance/conformance.env
|
||||
|
||||
# Generate the test users required by the CTH on the server to be tested
|
||||
npx ts-node test/deploy/createAccountCredentials.ts http://localhost:3000/ >> ../conformance/conformance.env
|
||||
|
||||
# Run the CTH
|
||||
docker run -i --rm \
|
||||
-v $(pwd)/../conformance/reports/css:/reports \
|
||||
--env-file=../conformance/conformance.env \
|
||||
--network="host" \
|
||||
solidproject/conformance-test-harness \
|
||||
--skip-teardown \
|
||||
--output=/reports \
|
||||
--target=https://github.com/solid/conformance-test-harness/css
|
||||
```
|
||||
|
||||
When this process is finished you can find the conformance report in the `../reports/css` folder.
|
36
documentation/markdown/usage/authorization-methods.md
Normal file
36
documentation/markdown/usage/authorization-methods.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Choosing the authorization method for your server
|
||||
|
||||
The CSS comes with support for two different authorization solutions:
|
||||
[Web Access Control (WAC)](https://solidproject.org/TR/wac)
|
||||
and [Access Control Policy (ACP)](https://solid.github.io/authorization-panel/acp-specification/).
|
||||
When configuring a server, one of these needs to be picked if you do not want everyone to have full access to your data.
|
||||
Both of these are similar in that they both make use of RDF resources to describe who can access which documents,
|
||||
|
||||
WAC is the older specification of the two,
|
||||
it was designed together with the beginning of the Solid specification.
|
||||
Because of that, there is more tooling available that can interpret the corresponding authorization resources,
|
||||
potentially making it easier to get started with Solid development.
|
||||
|
||||
ACP is a more recent specification,
|
||||
that was made to address certain concerns within WAC.
|
||||
ACP provides more options in how to define who gets to access your data,
|
||||
allowing you to have better security.
|
||||
|
||||
When using WAC, you define which WebIDs have access to certain data.
|
||||
When you then authenticate with a Solid client,
|
||||
that client will identify with your WebID,
|
||||
indicating to the server that it is allowed to access that data.
|
||||
The problem is that there is no (safe) way to differentiate between clients.
|
||||
This means that if you use a client to store your favorite movies in your pod,
|
||||
and another one to store your bank details,
|
||||
the movie client would be able to access your bank details if it was malicious.
|
||||
ACP on the other hand allows you to set more specific restrictions,
|
||||
where clients also have to identify themselves.
|
||||
This way you can make sure the movie client can only access movie data.
|
||||
|
||||
Currently, the CSS still enables WAC in most of the configurations bundled with the server,
|
||||
as we want the server to be easily accessible for newer users,
|
||||
for whom the chances are higher they are using apps only compatible with WAC.
|
||||
However, we are planning to eventually phase this out in favor of ACP,
|
||||
starting with logged warnings when WAC is enabled,
|
||||
and in the end changing the bundled configurations to use ACP instead.
|
@ -56,7 +56,7 @@ The next step generates the token and assumes you have an authorization value as
|
||||
```ts
|
||||
// Now that we are logged in, we need to request the updated controls from the server.
|
||||
// These will now have more values than in the previous example.
|
||||
const indexResponse = await fetch('http://localhost:3000/.account/', {
|
||||
const indexResponse = await fetch('http://localhost:3000/.account/', {
|
||||
headers: { authorization: `CSS-Account-Token ${authorization}` }
|
||||
});
|
||||
const { controls } = await indexResponse.json();
|
||||
|
@ -127,6 +127,31 @@ The response would then be something like this:
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming HTTP
|
||||
|
||||
Currently, Streaming HTTP channels are only available as pre-established channels on each resource.
|
||||
This means that subscribing and unsubscribing are not supported, and no subscription services are advertised.
|
||||
Instead, each resource advertises the `receiveFrom` of its pre-established notification channel using HTTP Link header,
|
||||
using `rel="http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023"`.
|
||||
|
||||
For example, this —
|
||||
|
||||
```shell
|
||||
curl --head 'http://localhost:3000/foo/'
|
||||
```
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Link: <http://localhost:3000/.notifications/StreamingHTTPChannel2023/foo/>; rel="http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023"
|
||||
```
|
||||
|
||||
It is essential to remember that any HTTP request to that `receiveFrom` endpoint requires the same authorization
|
||||
as a `GET` request on the resource which advertises it.
|
||||
|
||||
Currently, all pre-established Streaming HTTP channels have `Content-Type: text/turtle`.
|
||||
|
||||
Information on how to consume Streaming HTTP responses [is available on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#consuming_a_fetch_as_a_stream)
|
||||
|
||||
## Unsubscribing from a notification channel
|
||||
|
||||
!!! note
|
||||
|
@ -78,8 +78,10 @@ nav:
|
||||
- Welcome:
|
||||
- README.md
|
||||
- Features:
|
||||
- features.md
|
||||
- Overview: features.md
|
||||
- Tests: features/test.md
|
||||
- Usage:
|
||||
- Starting the server: usage/starting-server.md
|
||||
- Example request: usage/example-requests.md
|
||||
- Metadata: usage/metadata.md
|
||||
- Identity provider:
|
||||
@ -91,6 +93,7 @@ nav:
|
||||
- Seeding pods: usage/seeding-pods.md
|
||||
- Notifications: usage/notifications.md
|
||||
- Development server: usage/dev-configuration.md
|
||||
- Authorization methods: usage/authorization-methods.md
|
||||
- Architecture:
|
||||
- Overview: architecture/overview.md
|
||||
- Dependency injection: architecture/dependency-injection.md
|
||||
|
@ -1,45 +1,11 @@
|
||||
const antfu = require('@antfu/eslint-config');
|
||||
const generalConfig = require('./eslint/general');
|
||||
const testConfig = require('./eslint/test');
|
||||
const typedConfig = require('./eslint/typed');
|
||||
const unicornConfig = require('./eslint/unicorn');
|
||||
const opinionated = require('opinionated-eslint-config');
|
||||
|
||||
// The default ignore list contains all `output` folders, which conflicts with our src/http/output folder
|
||||
// See https://github.com/antfu/eslint-config/blob/7071af7024335aad319a91db41ce594ebc6a0899/src/globs.ts#L55
|
||||
const index = antfu.GLOB_EXCLUDE.indexOf('**/output');
|
||||
if (index < 0) {
|
||||
throw new Error('Could not update GLOB_EXCLUDE. Check if antfu changed how it handles ignores.');
|
||||
}
|
||||
antfu.GLOB_EXCLUDE.splice(index, 1);
|
||||
|
||||
module.exports = antfu.default(
|
||||
module.exports = opinionated(
|
||||
{
|
||||
// Don't want to lint test assets, or TS snippets in markdown files
|
||||
ignores: [ 'test/assets/*', '**/*.md/**/*.ts' ],
|
||||
},
|
||||
generalConfig,
|
||||
unicornConfig,
|
||||
typedConfig({
|
||||
project: [ './tsconfig.json', './scripts/tsconfig.json', './test/tsconfig.json' ],
|
||||
tsconfigRootDir: __dirname,
|
||||
}),
|
||||
testConfig,
|
||||
{
|
||||
// JSON rules
|
||||
files: [ '**/*.json' ],
|
||||
rules: {
|
||||
'jsonc/array-bracket-spacing': [ 'error', 'always', {
|
||||
singleValue: true,
|
||||
objectsInArrays: false,
|
||||
arraysInArrays: false,
|
||||
}],
|
||||
},
|
||||
},
|
||||
{
|
||||
// This is necessary to prevent filename checks caused by JSON being present in a README.
|
||||
files: [ '**/README.md/**' ],
|
||||
rules: {
|
||||
'unicorn/filename-case': 'off',
|
||||
ignores: [ 'test/assets/*', '**/*.md' ],
|
||||
typescript: {
|
||||
tsconfigPath: [ './tsconfig.json', './scripts/tsconfig.json', './test/tsconfig.json' ],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -1,114 +0,0 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'antfu/consistent-list-newline': 'error',
|
||||
|
||||
'arrow-body-style': [ 'error', 'as-needed', { requireReturnForObjectLiteral: false }],
|
||||
'capitalized-comments': [ 'error', 'always', { ignoreConsecutiveComments: true }],
|
||||
curly: [ 'error', 'all' ],
|
||||
'default-case': 'error',
|
||||
eqeqeq: [ 'error', 'always' ],
|
||||
'for-direction': 'error',
|
||||
'func-style': [ 'error', 'declaration' ],
|
||||
'function-call-argument-newline': [ 'error', 'consistent' ],
|
||||
'function-paren-newline': [ 'error', 'consistent' ],
|
||||
'getter-return': [ 'error', { allowImplicit: true }],
|
||||
'grouped-accessor-pairs': [ 'error', 'getBeforeSet' ],
|
||||
'guard-for-in': 'error',
|
||||
'line-comment-position': [ 'error', { position: 'above' }],
|
||||
'linebreak-style': [ 'error', 'unix' ],
|
||||
'multiline-comment-style': [ 'error', 'separate-lines' ],
|
||||
// Need to override `allow` value
|
||||
'no-console': [ 'error', { allow: [ '' ]}],
|
||||
'no-constructor-return': 'error',
|
||||
'no-dupe-else-if': 'error',
|
||||
'no-else-return': [ 'error', { allowElseIf: false }],
|
||||
'no-implicit-coercion': 'error',
|
||||
'no-implicit-globals': 'error',
|
||||
'no-lonely-if': 'error',
|
||||
'no-plusplus': [ 'error', { allowForLoopAfterthoughts: true }],
|
||||
'no-sync': [ 'error', { allowAtRootLevel: false }],
|
||||
'no-useless-concat': 'error',
|
||||
'no-useless-escape': 'error',
|
||||
'operator-assignment': [ 'error', 'always' ],
|
||||
'prefer-object-spread': 'error',
|
||||
radix: 'error',
|
||||
'require-unicode-regexp': 'error',
|
||||
'require-yield': 'error',
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
allowSeparatedGroups: false,
|
||||
ignoreCase: true,
|
||||
ignoreDeclarationSort: true,
|
||||
ignoreMemberSort: false,
|
||||
memberSyntaxSortOrder: [ 'none', 'all', 'multiple', 'single' ],
|
||||
},
|
||||
],
|
||||
|
||||
'import/extensions': 'error',
|
||||
|
||||
'jsdoc/no-multi-asterisks': [ 'error', { allowWhitespace: true }],
|
||||
|
||||
'node/prefer-global/buffer': 'off',
|
||||
'node/prefer-global/process': 'off',
|
||||
|
||||
'style/array-bracket-spacing': [ 'error', 'always', {
|
||||
singleValue: true,
|
||||
objectsInArrays: false,
|
||||
arraysInArrays: false,
|
||||
}],
|
||||
// Conflicts with style/object-curly-spacing
|
||||
'style/block-spacing': 'off',
|
||||
'style/brace-style': [ 'error', '1tbs', { allowSingleLine: false }],
|
||||
'style/generator-star-spacing': [ 'error', { before: false, after: true }],
|
||||
// Seems to be inconsistent in when it adds indentation and when it does not
|
||||
'style/indent-binary-ops': 'off',
|
||||
'style/member-delimiter-style': [ 'error', {
|
||||
multiline: { delimiter: 'semi', requireLast: true },
|
||||
singleline: { delimiter: 'semi', requireLast: false },
|
||||
}],
|
||||
'style/no-extra-parens': [ 'error', 'functions' ],
|
||||
'style/object-curly-spacing': [ 'error', 'always', {
|
||||
objectsInObjects: false,
|
||||
arraysInObjects: false,
|
||||
}],
|
||||
'style/operator-linebreak': [ 'error', 'after' ],
|
||||
'style/semi': [ 'error', 'always' ],
|
||||
'style/semi-style': [ 'error', 'last' ],
|
||||
'style/space-before-function-paren': [ 'error', 'never' ],
|
||||
'style/switch-colon-spacing': 'error',
|
||||
'style/quote-props': [ 'error', 'as-needed', {
|
||||
keywords: false,
|
||||
unnecessary: true,
|
||||
numbers: false,
|
||||
}],
|
||||
'style/yield-star-spacing': [ 'error', 'after' ],
|
||||
|
||||
'ts/adjacent-overload-signatures': 'error',
|
||||
'ts/array-type': 'error',
|
||||
'ts/ban-ts-comment': [ 'error', {
|
||||
'ts-expect-error': true,
|
||||
}],
|
||||
'ts/consistent-indexed-object-style': [ 'error', 'record' ],
|
||||
'ts/consistent-type-definitions': 'off',
|
||||
'ts/explicit-member-accessibility': 'error',
|
||||
'ts/method-signature-style': 'error',
|
||||
'ts/no-confusing-non-null-assertion': 'error',
|
||||
'ts/no-extraneous-class': [ 'error', {
|
||||
allowConstructorOnly: false,
|
||||
allowEmpty: false,
|
||||
allowStaticOnly: false,
|
||||
}],
|
||||
'ts/no-inferrable-types': [ 'error', {
|
||||
ignoreParameters: false,
|
||||
ignoreProperties: false,
|
||||
}],
|
||||
'ts/prefer-for-of': 'error',
|
||||
'ts/prefer-function-type': 'error',
|
||||
|
||||
'unused-imports/no-unused-vars': [
|
||||
'error',
|
||||
{ args: 'after-used', vars: 'all', ignoreRestSiblings: true },
|
||||
],
|
||||
},
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
const jest = require('eslint-plugin-jest');
|
||||
|
||||
// Specifically for tests
|
||||
module.exports = {
|
||||
// See https://github.com/jest-community/eslint-plugin-jest/issues/1408
|
||||
plugins: {
|
||||
jest,
|
||||
},
|
||||
files: [ 'test/**/*.ts' ],
|
||||
rules: {
|
||||
...jest.configs.all.rules,
|
||||
// Rule is not smart enough to check called function in the test
|
||||
'jest/expect-expect': 'off',
|
||||
'jest/valid-title': [ 'error', {
|
||||
mustNotMatch: {
|
||||
describe: /\.$/u.source,
|
||||
},
|
||||
mustMatch: {
|
||||
it: /\.$/u.source,
|
||||
},
|
||||
}],
|
||||
|
||||
// Default rules that are overkill
|
||||
'jest/no-hooks': 'off',
|
||||
'jest/max-expects': 'off',
|
||||
'jest/no-conditional-in-test': 'off',
|
||||
'jest/prefer-expect-assertions': 'off',
|
||||
'jest/prefer-lowercase-title': 'off',
|
||||
'jest/prefer-strict-equal': 'off',
|
||||
'jest/require-hook': 'off',
|
||||
|
||||
'test/prefer-lowercase-title': 'off',
|
||||
|
||||
'ts/naming-convention': 'off',
|
||||
'ts/no-unsafe-argument': 'off',
|
||||
'ts/no-unsafe-assignment': 'off',
|
||||
'ts/no-unsafe-call': 'off',
|
||||
'ts/no-unsafe-member-access': 'off',
|
||||
'ts/no-unsafe-return': 'off',
|
||||
'ts/unbound-method': 'off',
|
||||
|
||||
// Incorrectly detects usage of undefined in "toHaveBeenLastCalledWith" checks
|
||||
'unicorn/no-useless-undefined': 'off',
|
||||
},
|
||||
};
|
105
eslint/typed.js
105
eslint/typed.js
@ -1,105 +0,0 @@
|
||||
// Copied from https://github.com/antfu/eslint-config/blob/main/src/configs/typescript.ts
|
||||
// Doing it like this, so we can make sure these only try to trigger on *.ts files,
|
||||
// Preventing issues with the *.js files.
|
||||
const typeAwareRules = {
|
||||
'dot-notation': 'off',
|
||||
'no-implied-eval': 'off',
|
||||
'no-throw-literal': 'off',
|
||||
'ts/await-thenable': 'error',
|
||||
'ts/dot-notation': [ 'error', { allowKeywords: true }],
|
||||
'ts/no-floating-promises': 'error',
|
||||
'ts/no-for-in-array': 'error',
|
||||
'ts/no-implied-eval': 'error',
|
||||
'ts/no-misused-promises': 'error',
|
||||
'ts/no-throw-literal': 'error',
|
||||
'ts/no-unnecessary-type-assertion': 'error',
|
||||
'ts/no-unsafe-argument': 'error',
|
||||
'ts/no-unsafe-assignment': 'error',
|
||||
'ts/no-unsafe-call': 'error',
|
||||
'ts/no-unsafe-member-access': 'error',
|
||||
'ts/no-unsafe-return': 'error',
|
||||
'ts/restrict-plus-operands': 'error',
|
||||
'ts/restrict-template-expressions': 'error',
|
||||
'ts/unbound-method': 'error',
|
||||
};
|
||||
|
||||
const defaults = {
|
||||
project: [ './tsconfig.json' ],
|
||||
files: [ '**/*.ts' ],
|
||||
tsconfigRootDir: process.cwd(),
|
||||
};
|
||||
|
||||
module.exports = function(options) {
|
||||
options = { ...defaults, ...options };
|
||||
return {
|
||||
// By default, antfu also triggers type rules on *.js files which causes all kinds of issues for us
|
||||
files: options.files,
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: options.tsconfigRootDir,
|
||||
project: options.project,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...typeAwareRules,
|
||||
'ts/consistent-type-assertions': [ 'error', {
|
||||
assertionStyle: 'as',
|
||||
}],
|
||||
'ts/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'default',
|
||||
format: [ 'camelCase' ],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'import',
|
||||
format: null,
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: [ 'camelCase', 'UPPER_CASE' ],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: [ 'PascalCase' ],
|
||||
},
|
||||
{
|
||||
selector: [ 'typeParameter' ],
|
||||
format: [ 'PascalCase' ],
|
||||
prefix: [ 'T' ],
|
||||
},
|
||||
],
|
||||
'ts/explicit-function-return-type': [ 'error', {
|
||||
allowExpressions: false,
|
||||
allowTypedFunctionExpressions: false,
|
||||
allowHigherOrderFunctions: false,
|
||||
}],
|
||||
'ts/no-base-to-string': 'error',
|
||||
'ts/no-floating-promises': [ 'error', { ignoreVoid: false }],
|
||||
'ts/promise-function-async': 'error',
|
||||
'ts/no-unnecessary-boolean-literal-compare': 'error',
|
||||
'ts/no-unnecessary-qualifier': 'error',
|
||||
'ts/prefer-nullish-coalescing': 'error',
|
||||
'ts/prefer-readonly': 'error',
|
||||
'ts/prefer-reduce-type-parameter': 'error',
|
||||
'ts/prefer-regexp-exec': 'error',
|
||||
'ts/prefer-string-starts-ends-with': 'error',
|
||||
'ts/require-array-sort-compare': 'error',
|
||||
|
||||
// These are not type specific, but we only care about these in TS files
|
||||
'max-len': [ 'error', { code: 120, ignoreUrls: true }],
|
||||
'unicorn/filename-case': [ 'error', {
|
||||
cases: {
|
||||
camelCase: true,
|
||||
pascalCase: true,
|
||||
kebabCase: false,
|
||||
snakeCase: false,
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
};
|
@ -1,60 +0,0 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'unicorn/better-regex': 'error',
|
||||
'unicorn/empty-brace-spaces': 'error',
|
||||
'unicorn/consistent-function-scoping': 'error',
|
||||
'unicorn/expiring-todo-comments': [ 'error', {
|
||||
ignoreDatesOnPullRequests: false,
|
||||
terms: [ 'todo' ],
|
||||
allowWarningComments: false,
|
||||
}],
|
||||
'unicorn/explicit-length-check': 'error',
|
||||
'unicorn/filename-case': [ 'error', {
|
||||
cases: {
|
||||
camelCase: false,
|
||||
pascalCase: false,
|
||||
kebabCase: true,
|
||||
snakeCase: false,
|
||||
},
|
||||
}],
|
||||
'unicorn/new-for-builtins': 'error',
|
||||
'unicorn/no-array-for-each': 'error',
|
||||
'unicorn/no-array-reduce': 'error',
|
||||
'unicorn/no-for-loop': 'error',
|
||||
'unicorn/no-invalid-remove-event-listener': 'error',
|
||||
'unicorn/no-lonely-if': 'error',
|
||||
'unicorn/no-negated-condition': 'error',
|
||||
'unicorn/no-nested-ternary': 'error',
|
||||
'unicorn/no-object-as-default-parameter': 'error',
|
||||
'unicorn/no-process-exit': 'error',
|
||||
'unicorn/no-thenable': 'error',
|
||||
'unicorn/no-useless-fallback-in-spread': 'error',
|
||||
'unicorn/no-useless-length-check': 'error',
|
||||
'unicorn/no-useless-promise-resolve-reject': 'error',
|
||||
'unicorn/no-useless-spread': 'error',
|
||||
'unicorn/no-useless-undefined': 'error',
|
||||
'unicorn/no-zero-fractions': 'error',
|
||||
'unicorn/prefer-array-find': 'error',
|
||||
'unicorn/prefer-array-flat-map': 'error',
|
||||
'unicorn/prefer-array-index-of': 'error',
|
||||
'unicorn/prefer-array-some': 'error',
|
||||
'unicorn/prefer-at': 'error',
|
||||
'unicorn/prefer-code-point': 'error',
|
||||
'unicorn/prefer-date-now': 'error',
|
||||
'unicorn/prefer-default-parameters': 'error',
|
||||
'unicorn/prefer-math-trunc': 'error',
|
||||
'unicorn/prefer-native-coercion-functions': 'error',
|
||||
'unicorn/prefer-negative-index': 'error',
|
||||
'unicorn/prefer-object-from-entries': 'error',
|
||||
'unicorn/prefer-optional-catch-binding': 'error',
|
||||
'unicorn/prefer-reflect-apply': 'error',
|
||||
'unicorn/prefer-regexp-test': 'error',
|
||||
'unicorn/prefer-set-has': 'error',
|
||||
'unicorn/prefer-set-size': 'error',
|
||||
'unicorn/prefer-spread': 'error',
|
||||
'unicorn/prefer-string-replace-all': 'error',
|
||||
'unicorn/prefer-string-slice': 'error',
|
||||
'unicorn/require-array-join-separator': 'error',
|
||||
'unicorn/require-number-to-fixed-digits-argument': 'error',
|
||||
},
|
||||
};
|
@ -70,7 +70,7 @@ module.exports = {
|
||||
'^jose/(.*)$': '<rootDir>/node_modules/jose/dist/node/cjs/$1',
|
||||
},
|
||||
// Slower machines had problems calling the WebSocket integration callbacks on time
|
||||
testTimeout: 60000,
|
||||
testTimeout: 90000,
|
||||
|
||||
reporters: ci ? [ 'default', 'github-actions' ] : [ 'default' ],
|
||||
...ci && jestGithubRunnerSpecs(),
|
||||
|
6885
package-lock.json
generated
6885
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@solid/community-server",
|
||||
"version": "7.0.5",
|
||||
"version": "7.1.2",
|
||||
"description": "Community Solid Server: an open and modular implementation of the Solid specifications",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/CommunitySolidServer/CommunitySolidServer#readme",
|
||||
@ -56,7 +56,6 @@
|
||||
"lint": "npm run lint:eslint && npm run lint:markdown",
|
||||
"lint:eslint": "eslint . --cache --max-warnings 0",
|
||||
"lint:markdown": "markdownlint-cli2",
|
||||
"lint:markdown:fix": "markdownlint-cli2-fix",
|
||||
"prepare": "npm run build",
|
||||
"release": "commit-and-tag-version",
|
||||
"postrelease": "ts-node ./scripts/finalizeRelease.ts",
|
||||
@ -143,32 +142,31 @@
|
||||
"yup": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "2.3.4",
|
||||
"@commitlint/cli": "^17.7.2",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@inrupt/solid-client-authn-core": "^2.0.0",
|
||||
"@inrupt/solid-client-authn-node": "^2.0.0",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/set-cookie-parser": "^2.4.4",
|
||||
"@types/supertest": "^2.0.14",
|
||||
"commit-and-tag-version": "^11.3.0",
|
||||
"componentsjs-generator": "^3.1.2",
|
||||
"eslint-plugin-jest": "^27.4.3",
|
||||
"husky": "^4.3.8",
|
||||
"jest": "^29.7.0",
|
||||
"jest-esm-transformer-2": "^1.0.0",
|
||||
"jest-rdf": "^1.8.0",
|
||||
"markdownlint-cli2": "^0.10.0",
|
||||
"markdownlint-cli2": "^0.13.0",
|
||||
"node-mocks-http": "^1.13.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"opinionated-eslint-config": "0.1.0",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"simple-git": "^3.20.0",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.25.2",
|
||||
"typescript": "^5.2.2"
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.4",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -179,7 +177,7 @@
|
||||
"commit-and-tag-version": {
|
||||
"scripts": {
|
||||
"postbump": "ts-node ./scripts/upgradeConfig.ts",
|
||||
"postchangelog": "ts-node ./scripts/formatChangelog.ts && markdownlint-cli2-fix"
|
||||
"postchangelog": "ts-node ./scripts/formatChangelog.ts && markdownlint-cli2 --fix"
|
||||
},
|
||||
"writerOpts": {
|
||||
"commitsSort": false
|
||||
|
@ -27,7 +27,9 @@ async function commitAndTag(): Promise<void> {
|
||||
|
||||
/**
|
||||
* Prompts the user for input
|
||||
*
|
||||
* @param query - A string to prompt the user
|
||||
*
|
||||
* @returns Promise with the input of the user
|
||||
*/
|
||||
async function waitForUserInput(query: string): Promise<string> {
|
||||
|
@ -14,11 +14,13 @@ import { readFile, writeFile } from 'fs-extra';
|
||||
|
||||
/**
|
||||
* Capitalize all list entries
|
||||
*
|
||||
* @param input - String to search/replace
|
||||
*
|
||||
* @returns Promise with output string
|
||||
*/
|
||||
async function capitalizeListEntries(input: string): Promise<string> {
|
||||
return input.replaceAll(/^(\W*\* [a-z])/gmu, (match): string => match.toUpperCase());
|
||||
return input.replaceAll(/^\W*\* [a-z]/gmu, (match): string => match.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,7 +33,9 @@ function endProcess(error: Error): never {
|
||||
|
||||
/**
|
||||
* Main function for changelog formatting
|
||||
*
|
||||
* @param filePath - Path to the changelog file
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
async function formatChangelog(filePath: string): Promise<void> {
|
||||
|
@ -18,6 +18,7 @@ import { joinFilePath, readPackageJson } from '../src/util/PathUtil';
|
||||
|
||||
/**
|
||||
* Search and replace the version of a component with given name
|
||||
*
|
||||
* @param filePath - File to search/replace
|
||||
* @param regex - RegExp matching the component reference
|
||||
* @param version - Semantic version to change to
|
||||
@ -31,8 +32,10 @@ async function replaceComponentVersion(filePath: string, regex: RegExp, version:
|
||||
|
||||
/**
|
||||
* Recursive search for files that match a given Regex
|
||||
*
|
||||
* @param path - Path of folder to start search in
|
||||
* @param regex - A regular expression to which file names will be matched
|
||||
*
|
||||
* @returns Promise with all file pathss
|
||||
*/
|
||||
async function getFilePaths(path: string, regex: RegExp): Promise<string[]> {
|
||||
|
@ -19,7 +19,7 @@ export class UnsecureWebIdExtractor extends CredentialsExtractor {
|
||||
}
|
||||
|
||||
public async handle({ headers }: HttpRequest): Promise<Credentials> {
|
||||
const webId = /^WebID\s+(.*)/ui.exec(headers.authorization!)![1];
|
||||
const webId = /^WebID\s+(.*)/iu.exec(headers.authorization!)![1];
|
||||
this.logger.info(`Agent unsecurely claims to be ${webId}`);
|
||||
return { agent: { webId }};
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import { AclMode } from './permissions/AclPermissionSet';
|
||||
import { AccessMode } from './permissions/Permissions';
|
||||
import type { PermissionMap, PermissionSet } from './permissions/Permissions';
|
||||
|
||||
const modesMap: Record<string, Readonly<(keyof AclPermissionSet)[]>> = {
|
||||
const modesMap: Record<string, readonly (keyof AclPermissionSet)[]> = {
|
||||
[ACL.Read]: [ AccessMode.read ],
|
||||
[ACL.Write]: [ AccessMode.append, AccessMode.write ],
|
||||
[ACL.Append]: [ AccessMode.append ],
|
||||
@ -65,6 +65,7 @@ export class AcpReader extends PermissionReader {
|
||||
|
||||
/**
|
||||
* Generates the allowed permissions.
|
||||
*
|
||||
* @param target - Target to generate permissions for.
|
||||
* @param credentials - Credentials that are trying to access the resource.
|
||||
* @param resourceCache - Cache used to store ACR data.
|
||||
|
@ -27,6 +27,7 @@ function getObjectValues(data: Store, subject: Term, predicate: NamedNode): stri
|
||||
|
||||
/**
|
||||
* Finds the {@link IMatcher} with the given identifier in the given dataset.
|
||||
*
|
||||
* @param data - Dataset to look in.
|
||||
* @param matcher - Identifier of the matcher.
|
||||
*/
|
||||
@ -42,6 +43,7 @@ export function getMatcher(data: Store, matcher: Term): IMatcher {
|
||||
|
||||
/**
|
||||
* Finds the {@link IPolicy} with the given identifier in the given dataset.
|
||||
*
|
||||
* @param data - Dataset to look in.
|
||||
* @param policy - Identifier of the policy.
|
||||
*/
|
||||
@ -58,6 +60,7 @@ export function getPolicy(data: Store, policy: Term): IPolicy {
|
||||
|
||||
/**
|
||||
* Finds the {@link IAccessControl} with the given identifier in the given dataset.
|
||||
*
|
||||
* @param data - Dataset to look in.
|
||||
* @param accessControl - Identifier of the access control.
|
||||
*/
|
||||
@ -71,6 +74,7 @@ export function getAccessControl(data: Store, accessControl: Term): IAccessContr
|
||||
|
||||
/**
|
||||
* Finds the {@link IAccessControlResource} with the given identifier in the given dataset.
|
||||
*
|
||||
* @param data - Dataset to look in.
|
||||
* @param acr - Identifier of the access control resource.
|
||||
*/
|
||||
@ -88,6 +92,7 @@ export function getAccessControlResource(data: Store, acr: Term): IAccessControl
|
||||
|
||||
/**
|
||||
* Finds all {@link IAccessControlledResource} in the given dataset.
|
||||
*
|
||||
* @param data - Dataset to look in.
|
||||
*/
|
||||
export function* getAccessControlledResources(data: Store): Iterable<IAccessControlledResource> {
|
||||
|
@ -102,7 +102,9 @@ export class ParentContainerReader extends PermissionReader {
|
||||
// When an operation requests to delete a resource,
|
||||
// the server MUST match Authorizations allowing the acl:Write access privilege
|
||||
// on the resource and the containing container.
|
||||
mergedPermission.delete = resourcePermission.write && containerPermission.write &&
|
||||
mergedPermission.delete =
|
||||
resourcePermission.write &&
|
||||
containerPermission.write &&
|
||||
resourcePermission.delete !== false;
|
||||
|
||||
return mergedPermission;
|
||||
|
@ -24,6 +24,7 @@ export class PermissionBasedAuthorizer extends Authorizer {
|
||||
/**
|
||||
* The existence of the target resource determines the output status code for certain situations.
|
||||
* The provided {@link ResourceSet} will be used for that.
|
||||
*
|
||||
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
|
||||
*/
|
||||
public constructor(resourceSet: ResourceSet) {
|
||||
@ -77,6 +78,7 @@ export class PermissionBasedAuthorizer extends Authorizer {
|
||||
* Ensures that at least one of the credentials provides permissions for the given mode.
|
||||
* Throws a {@link ForbiddenHttpError} or {@link UnauthorizedHttpError} depending on the credentials
|
||||
* if access is not allowed.
|
||||
*
|
||||
* @param credentials - Credentials that require access.
|
||||
* @param permissionSet - PermissionSet describing the available permissions of the credentials.
|
||||
* @param mode - Which mode is requested.
|
||||
@ -98,6 +100,7 @@ export class PermissionBasedAuthorizer extends Authorizer {
|
||||
|
||||
/**
|
||||
* Checks whether the agent is authenticated (logged in) or not (public/anonymous).
|
||||
*
|
||||
* @param credentials - Credentials to check.
|
||||
*/
|
||||
private isAuthenticated(credentials: Credentials): boolean {
|
||||
|
@ -22,7 +22,7 @@ import type { PermissionMap } from './permissions/Permissions';
|
||||
import { AccessMode } from './permissions/Permissions';
|
||||
|
||||
// Maps WebACL-specific modes to generic access modes.
|
||||
const modesMap: Record<string, Readonly<(keyof AclPermissionSet)[]>> = {
|
||||
const modesMap: Record<string, readonly (keyof AclPermissionSet)[]> = {
|
||||
[ACL.Read]: [ AccessMode.read ],
|
||||
[ACL.Write]: [ AccessMode.append, AccessMode.write ],
|
||||
[ACL.Append]: [ AccessMode.append ],
|
||||
@ -69,7 +69,7 @@ export class WebAclReader extends PermissionReader {
|
||||
this.logger.debug(`Retrieving permissions of ${credentials.agent?.webId ?? 'an unknown agent'}`);
|
||||
const aclMap = await this.getAclMatches(requestedModes.distinctKeys());
|
||||
const storeMap = await this.findAuthorizationStatements(aclMap);
|
||||
return await this.findPermissions(storeMap, credentials);
|
||||
return this.findPermissions(storeMap, credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,6 +96,7 @@ export class WebAclReader extends PermissionReader {
|
||||
|
||||
/**
|
||||
* Determines the available permissions for the given credentials.
|
||||
*
|
||||
* @param acl - Store containing all relevant authorization triples.
|
||||
* @param credentials - Credentials to find the permissions for.
|
||||
*/
|
||||
@ -223,6 +224,7 @@ export class WebAclReader extends PermissionReader {
|
||||
/**
|
||||
* Extracts all rules from the store that are relevant for the given target,
|
||||
* based on either the `acl:accessTo` or `acl:default` predicates.
|
||||
*
|
||||
* @param store - Store to filter.
|
||||
* @param target - The identifier of which the acl rules need to be known.
|
||||
* @param directAcl - If the store contains triples from the direct acl resource of the target or not.
|
||||
|
@ -21,7 +21,7 @@ export class AgentGroupAccessChecker extends AccessChecker {
|
||||
const { webId } = credentials.agent;
|
||||
const groups = acl.getObjects(rule, ACL.terms.agentGroup, null);
|
||||
|
||||
return await promiseSome(groups.map(async(group: Term): Promise<boolean> =>
|
||||
return promiseSome(groups.map(async(group: Term): Promise<boolean> =>
|
||||
this.isMemberOfGroup(webId, group)));
|
||||
}
|
||||
return false;
|
||||
@ -29,6 +29,7 @@ export class AgentGroupAccessChecker extends AccessChecker {
|
||||
|
||||
/**
|
||||
* Checks if the given agent is member of a given vCard group.
|
||||
*
|
||||
* @param webId - WebID of the agent that needs access.
|
||||
* @param group - URL of the vCard group that needs to be checked.
|
||||
*
|
||||
@ -50,6 +51,6 @@ export class AgentGroupAccessChecker extends AccessChecker {
|
||||
const representation = await fetchDataset(url);
|
||||
return readableToQuads(representation.data);
|
||||
})();
|
||||
return await prom;
|
||||
return prom;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ export class IntermediateCreateExtractor extends ModesExtractor {
|
||||
/**
|
||||
* Certain permissions depend on the existence of the target resource.
|
||||
* The provided {@link ResourceSet} will be used for that.
|
||||
*
|
||||
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
|
||||
* @param strategy - {@link IdentifierStrategy} that will be used to determine parent containers.
|
||||
* @param source - The source {@link ModesExtractor}.
|
||||
|
@ -20,6 +20,7 @@ export class MethodModesExtractor extends ModesExtractor {
|
||||
/**
|
||||
* Certain permissions depend on the existence of the target resource.
|
||||
* The provided {@link ResourceSet} will be used for that.
|
||||
*
|
||||
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
|
||||
*/
|
||||
public constructor(resourceSet: ResourceSet) {
|
||||
|
@ -22,6 +22,7 @@ export class N3PatchModesExtractor extends ModesExtractor {
|
||||
/**
|
||||
* Certain permissions depend on the existence of the target resource.
|
||||
* The provided {@link ResourceSet} will be used for that.
|
||||
*
|
||||
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
|
||||
*/
|
||||
public constructor(resourceSet: ResourceSet) {
|
||||
|
@ -20,6 +20,7 @@ export class SparqlUpdateModesExtractor extends ModesExtractor {
|
||||
/**
|
||||
* Certain permissions depend on the existence of the target resource.
|
||||
* The provided {@link ResourceSet} will be used for that.
|
||||
*
|
||||
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
|
||||
*/
|
||||
public constructor(resourceSet: ResourceSet) {
|
||||
|
@ -79,7 +79,7 @@ class WebSocketListener extends WebSocketListenerEmitter {
|
||||
|
||||
private onMessage(message: string): void {
|
||||
// Parse the message
|
||||
const match = /^(\w+)\s+(.+)$/u.exec(message);
|
||||
const match = /^(\w+)\s+(\S.+)$/u.exec(message);
|
||||
if (!match) {
|
||||
this.sendMessage('warning', `Unrecognized message format: ${message}`);
|
||||
return;
|
||||
|
@ -31,6 +31,7 @@ export interface AuxiliaryIdentifierStrategy {
|
||||
* Checks if the input identifier corresponds to an auxiliary resource.
|
||||
* This does not check if that auxiliary resource exists,
|
||||
* only if the identifier indicates that there could be an auxiliary resource there.
|
||||
*
|
||||
* @param identifier - Identifier to check.
|
||||
*
|
||||
* @returns true if the input identifier points to an auxiliary resource.
|
||||
@ -40,6 +41,7 @@ export interface AuxiliaryIdentifierStrategy {
|
||||
/**
|
||||
* Returns the identifier of the resource which this auxiliary resource is referring to.
|
||||
* This does not guarantee that this resource exists.
|
||||
*
|
||||
* @param identifier - Identifier of the auxiliary resource.
|
||||
*
|
||||
* @returns The ResourceIdentifier of the subject resource.
|
||||
|
@ -11,6 +11,7 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
|
||||
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
|
||||
/**
|
||||
* Whether this auxiliary resources uses its own authorization instead of the subject resource authorization.
|
||||
*
|
||||
* @param identifier - Identifier of the auxiliary resource.
|
||||
*/
|
||||
usesOwnAuthorization: (identifier: ResourceIdentifier) => boolean;
|
||||
@ -18,6 +19,7 @@ export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
|
||||
/**
|
||||
* Whether the root storage container requires this auxiliary resource to be present.
|
||||
* If yes, this means they can't be deleted individually from such a container.
|
||||
*
|
||||
* @param identifier - Identifier of the auxiliary resource.
|
||||
*/
|
||||
isRequiredInRoot: (identifier: ResourceIdentifier) => boolean;
|
||||
@ -42,6 +44,7 @@ export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
|
||||
/**
|
||||
* Validates if the representation contains valid data for an auxiliary resource.
|
||||
* Should throw an error in case the data is invalid.
|
||||
*
|
||||
* @param identifier - Identifier of the auxiliary resource.
|
||||
* @param representation - Representation of the auxiliary resource.
|
||||
*/
|
||||
|
@ -50,7 +50,9 @@ export class LinkRelObject {
|
||||
|
||||
/**
|
||||
* Checks whether the object can be added to the metadata
|
||||
*
|
||||
* @param object - The link target.
|
||||
*
|
||||
* @returns a boolean to indicate whether it can be added to the metadata or not
|
||||
*/
|
||||
private objectAllowed(object: string): boolean {
|
||||
@ -59,6 +61,7 @@ export class LinkRelObject {
|
||||
|
||||
/**
|
||||
* Adds the object to the metadata when it is allowed
|
||||
*
|
||||
* @param object - The link target.
|
||||
* @param metadata - Metadata of the resource.
|
||||
* @param logger - Logger
|
||||
@ -67,8 +70,10 @@ export class LinkRelObject {
|
||||
if (this.objectAllowed(object)) {
|
||||
if (this.ephemeral) {
|
||||
metadata.add(this.value, namedNode(object), SOLID_META.terms.ResponseMetadata);
|
||||
logger.debug(`"<${metadata.identifier.value}> <${this.value.value}> <${object}>." ` +
|
||||
`will not be stored permanently in the metadata.`);
|
||||
logger.debug(
|
||||
`"<${metadata.identifier.value}> <${this.value.value}> <${object}>." ` +
|
||||
`will not be stored permanently in the metadata.`,
|
||||
);
|
||||
} else {
|
||||
metadata.add(this.value, namedNode(object));
|
||||
}
|
||||
|
@ -62,8 +62,8 @@ export class ConvertingErrorHandler extends ErrorHandler {
|
||||
private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> {
|
||||
if (!this.showStackTrace) {
|
||||
delete error.stack;
|
||||
// eslint-disable-next-line ts/no-unsafe-member-access
|
||||
delete (error as any).cause;
|
||||
// Cheating here to delete a readonly field
|
||||
delete (error as { cause: unknown }).cause;
|
||||
}
|
||||
const representation = new BasicRepresentation([ error ], error.metadata, INTERNAL_ERROR, false);
|
||||
const identifier = { path: representation.metadata.identifier.value };
|
||||
|
30
src/http/output/error/TargetExtractorErrorHandler.ts
Normal file
30
src/http/output/error/TargetExtractorErrorHandler.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { DataFactory } from 'n3';
|
||||
import { SOLID_ERROR } from '../../../util/Vocabularies';
|
||||
import type { TargetExtractor } from '../../input/identifier/TargetExtractor';
|
||||
import type { ResponseDescription } from '../response/ResponseDescription';
|
||||
import type { ErrorHandlerArgs } from './ErrorHandler';
|
||||
import { ErrorHandler } from './ErrorHandler';
|
||||
|
||||
/**
|
||||
* Adds metadata to an error to indicate the identifier of the originally targeted resource.
|
||||
*/
|
||||
export class TargetExtractorErrorHandler extends ErrorHandler {
|
||||
protected readonly errorHandler: ErrorHandler;
|
||||
protected readonly targetExtractor: TargetExtractor;
|
||||
|
||||
public constructor(errorHandler: ErrorHandler, targetExtractor: TargetExtractor) {
|
||||
super();
|
||||
this.errorHandler = errorHandler;
|
||||
this.targetExtractor = targetExtractor;
|
||||
}
|
||||
|
||||
public async canHandle(input: ErrorHandlerArgs): Promise<void> {
|
||||
return this.errorHandler.canHandle(input);
|
||||
}
|
||||
|
||||
public async handle(input: ErrorHandlerArgs): Promise<ResponseDescription> {
|
||||
const target = await this.targetExtractor.handleSafe(input);
|
||||
input.error.metadata.add(SOLID_ERROR.terms.target, DataFactory.namedNode(target.path));
|
||||
return this.errorHandler.handle(input);
|
||||
}
|
||||
}
|
@ -8,8 +8,11 @@ import { LDP, PIM, RDF, SOLID_ERROR } from '../../../util/Vocabularies';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
// Only PUT and PATCH can be used to create a new resource
|
||||
const NEW_RESOURCE_ALLOWED_METHODS = new Set([ 'PUT', 'PATCH' ]);
|
||||
enum ResourceType {
|
||||
document,
|
||||
container,
|
||||
unknown,
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Allow, Accept-Patch, Accept-Post, and Accept-Put headers.
|
||||
@ -30,8 +33,20 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||
const { response, metadata } = input;
|
||||
|
||||
let resourceType: ResourceType;
|
||||
if (metadata.has(RDF.terms.type, LDP.terms.Resource)) {
|
||||
resourceType = isContainerPath(metadata.identifier.value) ? ResourceType.container : ResourceType.document;
|
||||
} else {
|
||||
const target = metadata.get(SOLID_ERROR.terms.target)?.value;
|
||||
if (target) {
|
||||
resourceType = isContainerPath(target) ? ResourceType.container : ResourceType.document;
|
||||
} else {
|
||||
resourceType = ResourceType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out methods which are not allowed
|
||||
const allowedMethods = this.filterAllowedMethods(metadata);
|
||||
const allowedMethods = this.filterAllowedMethods(metadata, resourceType);
|
||||
|
||||
// Generate the Allow headers (if required)
|
||||
const generateAllow = this.generateAllow(allowedMethods, response, metadata);
|
||||
@ -43,29 +58,34 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
/**
|
||||
* Starts from the stored set of methods and removes all those that are not allowed based on the metadata.
|
||||
*/
|
||||
private filterAllowedMethods(metadata: RepresentationMetadata): Set<string> {
|
||||
private filterAllowedMethods(metadata: RepresentationMetadata, resourceType: ResourceType): Set<string> {
|
||||
const disallowedMethods = new Set(metadata.getAll(SOLID_ERROR.terms.disallowedMethod)
|
||||
.map((term): string => term.value));
|
||||
const allowedMethods = new Set(this.supportedMethods.filter((method): boolean => !disallowedMethods.has(method)));
|
||||
|
||||
// POST is only allowed on containers.
|
||||
// Metadata only has the resource URI in case it has resource metadata.
|
||||
if (!this.isPostAllowed(metadata)) {
|
||||
if (!this.isPostAllowed(resourceType)) {
|
||||
allowedMethods.delete('POST');
|
||||
}
|
||||
|
||||
if (!this.isPutAllowed(metadata)) {
|
||||
if (!this.isPutAllowed(metadata, resourceType)) {
|
||||
allowedMethods.delete('PUT');
|
||||
}
|
||||
|
||||
if (!this.isDeleteAllowed(metadata)) {
|
||||
if (!this.isPatchAllowed(resourceType)) {
|
||||
allowedMethods.delete('PATCH');
|
||||
}
|
||||
|
||||
if (!this.isDeleteAllowed(metadata, resourceType)) {
|
||||
allowedMethods.delete('DELETE');
|
||||
}
|
||||
|
||||
// If we are sure the resource does not exist: only keep methods that can create a new resource.
|
||||
if (metadata.has(SOLID_ERROR.terms.errorResponse, NotFoundHttpError.uri)) {
|
||||
for (const method of allowedMethods) {
|
||||
if (!NEW_RESOURCE_ALLOWED_METHODS.has(method)) {
|
||||
// Containers can only be created by PUT; documents by PUT or PATCH
|
||||
if (method !== 'PUT' && (method !== 'PATCH' || resourceType === ResourceType.container)) {
|
||||
allowedMethods.delete(method);
|
||||
}
|
||||
}
|
||||
@ -76,18 +96,23 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
|
||||
/**
|
||||
* POST is only allowed on containers.
|
||||
* The metadata URI is only valid in case there is resource metadata,
|
||||
* otherwise it is just a blank node.
|
||||
*/
|
||||
private isPostAllowed(metadata: RepresentationMetadata): boolean {
|
||||
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || isContainerPath(metadata.identifier.value);
|
||||
private isPostAllowed(resourceType: ResourceType): boolean {
|
||||
return resourceType !== ResourceType.document;
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT is not allowed on existing containers.
|
||||
*/
|
||||
private isPutAllowed(metadata: RepresentationMetadata): boolean {
|
||||
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || !isContainerPath(metadata.identifier.value);
|
||||
private isPutAllowed(metadata: RepresentationMetadata, resourceType: ResourceType): boolean {
|
||||
return resourceType !== ResourceType.container || !metadata.has(RDF.terms.type, LDP.terms.Resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH is not allowed on containers.
|
||||
*/
|
||||
private isPatchAllowed(resourceType: ResourceType): boolean {
|
||||
return resourceType !== ResourceType.container;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,14 +122,14 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
*
|
||||
* Note that the identifier value check only works if the metadata is not about an error.
|
||||
*/
|
||||
private isDeleteAllowed(metadata: RepresentationMetadata): boolean {
|
||||
if (!isContainerPath(metadata.identifier.value)) {
|
||||
private isDeleteAllowed(metadata: RepresentationMetadata, resourceType: ResourceType): boolean {
|
||||
if (resourceType !== ResourceType.container) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isStorage = metadata.has(RDF.terms.type, PIM.terms.Storage);
|
||||
const isEmpty = metadata.has(LDP.terms.contains);
|
||||
return !isStorage && !isEmpty;
|
||||
const isEmpty = !metadata.has(LDP.terms.contains);
|
||||
return !isStorage && isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Util } from 'n3';
|
||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import { addHeader } from '../../../util/HeaderUtil';
|
||||
import { LDP, RDF, SOLID_ERROR } from '../../../util/Vocabularies';
|
||||
import type { AuxiliaryStrategy } from '../../auxiliary/AuxiliaryStrategy';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../representation/ResourceIdentifier';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
/**
|
||||
@ -30,9 +31,17 @@ export class AuxiliaryLinkMetadataWriter extends MetadataWriter {
|
||||
}
|
||||
|
||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||
const identifier = { path: input.metadata.identifier.value };
|
||||
let identifier: ResourceIdentifier | undefined;
|
||||
if (input.metadata.has(RDF.terms.type, LDP.terms.Resource)) {
|
||||
identifier = { path: input.metadata.identifier.value };
|
||||
} else {
|
||||
const target = input.metadata.get(SOLID_ERROR.terms.target);
|
||||
if (target) {
|
||||
identifier = { path: target.value };
|
||||
}
|
||||
}
|
||||
// The metadata identifier will be a blank node in case an error was thrown.
|
||||
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) && !Util.isBlankNode(input.metadata.identifier)) {
|
||||
if (identifier && !this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
||||
const auxiliaryIdentifier = this.specificStrategy.getAuxiliaryIdentifier(identifier);
|
||||
addHeader(input.response, 'Link', `<${auxiliaryIdentifier.path}>; rel="${this.relationType}"`);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ export class BasicRepresentation implements Representation {
|
||||
* @param binary - Whether the representation is a binary or object stream
|
||||
*/
|
||||
public constructor(
|
||||
data: Guarded<Readable> | Readable | any[] | string,
|
||||
data: Guarded<Readable> | Readable | unknown[] | string,
|
||||
metadata: RepresentationMetadata | MetadataRecord,
|
||||
binary?: boolean,
|
||||
);
|
||||
@ -47,7 +47,7 @@ export class BasicRepresentation implements Representation {
|
||||
* @param binary - Whether the representation is a binary or object stream
|
||||
*/
|
||||
public constructor(
|
||||
data: Guarded<Readable> | Readable | any[] | string,
|
||||
data: Guarded<Readable> | Readable | unknown[] | string,
|
||||
metadata: RepresentationMetadata | MetadataRecord,
|
||||
contentType?: string,
|
||||
binary?: boolean,
|
||||
@ -59,7 +59,7 @@ export class BasicRepresentation implements Representation {
|
||||
* @param binary - Whether the representation is a binary or object stream
|
||||
*/
|
||||
public constructor(
|
||||
data: Guarded<Readable> | Readable | any[] | string,
|
||||
data: Guarded<Readable> | Readable | unknown[] | string,
|
||||
contentType: string,
|
||||
binary?: boolean,
|
||||
);
|
||||
@ -71,7 +71,7 @@ export class BasicRepresentation implements Representation {
|
||||
* @param binary - Whether the representation is a binary or object stream
|
||||
*/
|
||||
public constructor(
|
||||
data: Guarded<Readable> | Readable | any[] | string,
|
||||
data: Guarded<Readable> | Readable | unknown[] | string,
|
||||
identifier: MetadataIdentifier,
|
||||
metadata?: MetadataRecord,
|
||||
binary?: boolean,
|
||||
@ -84,14 +84,14 @@ export class BasicRepresentation implements Representation {
|
||||
* @param binary - Whether the representation is a binary or object stream
|
||||
*/
|
||||
public constructor(
|
||||
data: Guarded<Readable> | Readable | any[] | string,
|
||||
data: Guarded<Readable> | Readable | unknown[] | string,
|
||||
identifier: MetadataIdentifier,
|
||||
contentType?: string,
|
||||
binary?: boolean,
|
||||
);
|
||||
|
||||
public constructor(
|
||||
data?: Readable | any[] | string,
|
||||
data?: Readable | unknown[] | string,
|
||||
metadata?: RepresentationMetadata | MetadataRecord | MetadataIdentifier | string,
|
||||
metadataRest?: MetadataRecord | string | boolean,
|
||||
binary?: boolean,
|
||||
@ -109,8 +109,7 @@ export class BasicRepresentation implements Representation {
|
||||
}
|
||||
if (!isRepresentationMetadata(metadata) || typeof metadataRest === 'string') {
|
||||
// This combination will always match with a valid overload
|
||||
// eslint-disable-next-line ts/no-unsafe-argument
|
||||
metadata = new RepresentationMetadata(metadata as any, metadataRest as any);
|
||||
metadata = new RepresentationMetadata(metadata as RepresentationMetadata, metadataRest as string);
|
||||
}
|
||||
this.metadata = metadata;
|
||||
|
||||
|
@ -15,7 +15,7 @@ export type MetadataGraph = NamedNode | BlankNode | DefaultGraph | string;
|
||||
/**
|
||||
* Determines whether the object is a `RepresentationMetadata`.
|
||||
*/
|
||||
export function isRepresentationMetadata(object: any): object is RepresentationMetadata {
|
||||
export function isRepresentationMetadata(object: unknown): object is RepresentationMetadata {
|
||||
return typeof (object as RepresentationMetadata)?.setMetadata === 'function';
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ const cachedNamedNodes: Record<string, NamedNode> = {};
|
||||
* Converts the incoming name (URI or shorthand) to a named node.
|
||||
* The generated terms get cached to reduce the number of created nodes,
|
||||
* so only use this for internal constants!
|
||||
*
|
||||
* @param name - Predicate to potentially transform.
|
||||
*/
|
||||
function toCachedNamedNode(name: string): NamedNode {
|
||||
@ -167,6 +168,7 @@ export class RepresentationMetadata {
|
||||
/**
|
||||
* Helper function to import all entries from the given metadata.
|
||||
* If the new metadata has a different identifier the internal one will be updated.
|
||||
*
|
||||
* @param metadata - Metadata to import.
|
||||
*/
|
||||
public setMetadata(metadata: RepresentationMetadata): this {
|
||||
@ -235,22 +237,24 @@ export class RepresentationMetadata {
|
||||
|
||||
/**
|
||||
* Adds a value linked to the identifier. Strings get converted to literals.
|
||||
*
|
||||
* @param predicate - Predicate linking identifier to value.
|
||||
* @param object - Value(s) to add.
|
||||
* @param graph - Optional graph of where to add the values to.
|
||||
*/
|
||||
public add(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this {
|
||||
return this.forQuads(predicate, object, (pred, obj): any => this.addQuad(this.id, pred, obj, graph));
|
||||
return this.forQuads(predicate, object, (pred, obj): unknown => this.addQuad(this.id, pred, obj, graph));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given value from the metadata. Strings get converted to literals.
|
||||
*
|
||||
* @param predicate - Predicate linking identifier to value.
|
||||
* @param object - Value(s) to remove.
|
||||
* @param graph - Optional graph of where to remove the values from.
|
||||
*/
|
||||
public remove(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this {
|
||||
return this.forQuads(predicate, object, (pred, obj): any => this.removeQuad(this.id, pred, obj, graph));
|
||||
return this.forQuads(predicate, object, (pred, obj): unknown => this.removeQuad(this.id, pred, obj, graph));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -271,6 +275,7 @@ export class RepresentationMetadata {
|
||||
|
||||
/**
|
||||
* Removes all values linked through the given predicate.
|
||||
*
|
||||
* @param predicate - Predicate to remove.
|
||||
* @param graph - Optional graph where to remove from.
|
||||
*/
|
||||
@ -290,12 +295,17 @@ export class RepresentationMetadata {
|
||||
): boolean {
|
||||
// This works with N3.js but at the time of writing the typings have not been updated yet.
|
||||
// If you see this line of code check if the typings are already correct and update this if so.
|
||||
// eslint-disable-next-line ts/no-unsafe-call
|
||||
return (this.store.has as any)(this.id, predicate, object, graph) as boolean;
|
||||
return (this.store as unknown as {
|
||||
has: (subject: Term,
|
||||
predicate: Term | string | null,
|
||||
object: Term | string | null,
|
||||
graph: Term | string | null) => boolean;
|
||||
}).has(this.id, predicate, object, graph);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all object values matching the given predicate and/or graph.
|
||||
*
|
||||
* @param predicate - Optional predicate to get the values for.
|
||||
* @param graph - Optional graph where to get from.
|
||||
*
|
||||
@ -310,10 +320,10 @@ export class RepresentationMetadata {
|
||||
* @param predicate - Predicate to get the value for.
|
||||
* @param graph - Optional graph where the triple should be found.
|
||||
*
|
||||
* @returns The corresponding value. Undefined if there is no match
|
||||
*
|
||||
* @throws Error
|
||||
* If there are multiple matching values.
|
||||
*
|
||||
* @returns The corresponding value. Undefined if there is no match
|
||||
*/
|
||||
public get(predicate: NamedNode, graph?: MetadataGraph): Term | undefined {
|
||||
const terms = this.getAll(predicate, graph);
|
||||
@ -333,6 +343,7 @@ export class RepresentationMetadata {
|
||||
/**
|
||||
* Sets the value for the given predicate, removing all other instances.
|
||||
* In case the object is undefined this is identical to `removeAll(predicate)`.
|
||||
*
|
||||
* @param predicate - Predicate linking to the value.
|
||||
* @param object - Value(s) to set.
|
||||
* @param graph - Optional graph where the triple should be stored.
|
||||
@ -379,6 +390,7 @@ export class RepresentationMetadata {
|
||||
|
||||
/**
|
||||
* Parse the internal RDF structure to retrieve the Record with ContentType Parameters.
|
||||
*
|
||||
* @returns A {@link ContentType} object containing the value and optional parameters if there is one.
|
||||
*/
|
||||
private getContentType(): ContentType | undefined {
|
||||
|
@ -12,5 +12,5 @@ export interface ResourceIdentifier {
|
||||
* Determines whether the object is a {@link ResourceIdentifier}.
|
||||
*/
|
||||
export function isResourceIdentifier(object: unknown): object is ResourceIdentifier {
|
||||
return Boolean(object) && (typeof (object as ResourceIdentifier).path === 'string');
|
||||
return Boolean(object) && typeof (object as ResourceIdentifier).path === 'string';
|
||||
}
|
||||
|
@ -187,14 +187,20 @@ export class IdentityProviderFactory implements ProviderFactory {
|
||||
provider.use(async(ctx, next): Promise<void> => {
|
||||
const accepts = ctx.accepts.bind(ctx);
|
||||
|
||||
// Using `any` typings to make sure we support all different versions of `ctx.accepts`
|
||||
ctx.accepts = (...types): any => {
|
||||
// This is how you get the correct typing for an overloaded function
|
||||
type AcceptFn = {
|
||||
(): string[];
|
||||
(...types: string[]): string | false;
|
||||
(types: string[]): string | false;
|
||||
};
|
||||
|
||||
ctx.accepts = ((...types): string[] | string | false => {
|
||||
// Make sure we only override our specific case
|
||||
if (types.length === 2 && types[0] === 'json' && types[1] === 'html') {
|
||||
return 'html';
|
||||
}
|
||||
return accepts(...types as string[]);
|
||||
};
|
||||
}) as AcceptFn;
|
||||
|
||||
return next();
|
||||
});
|
||||
@ -254,10 +260,10 @@ export class IdentityProviderFactory implements ProviderFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given token is an access token.
|
||||
* Checks whether the given token is an access token.
|
||||
* The AccessToken interface is not exported, so we have to access it like this.
|
||||
*/
|
||||
private isAccessToken(token: any): token is KoaContextWithOIDC['oidc']['accessToken'] {
|
||||
private isAccessToken(token: unknown): token is KoaContextWithOIDC['oidc']['accessToken'] {
|
||||
return (token as KoaContextWithOIDC['oidc']['accessToken'])?.kind === 'AccessToken';
|
||||
}
|
||||
|
||||
@ -270,7 +276,7 @@ export class IdentityProviderFactory implements ProviderFactory {
|
||||
// Some fields are still missing, see https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1154#issuecomment-1040233385
|
||||
config.findAccount = async(ctx: KoaContextWithOIDC, sub: string): Promise<Account> => ({
|
||||
accountId: sub,
|
||||
async claims(): Promise<{ sub: string; [key: string]: any }> {
|
||||
async claims(): Promise<{ sub: string; [key: string]: unknown }> {
|
||||
return { sub, webid: sub, azp: ctx.oidc.client?.clientId };
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { Json } from '../../util/Json';
|
||||
import { ACCOUNT_ID_KEY } from './account/AccountIdRoute';
|
||||
import type { Json, JsonRepresentation } from './InteractionUtil';
|
||||
import type { JsonRepresentation } from './InteractionUtil';
|
||||
import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
|
||||
import { JsonInteractionHandler } from './JsonInteractionHandler';
|
||||
import type { InteractionRoute } from './routing/InteractionRoute';
|
||||
|
@ -3,16 +3,12 @@ import type Provider from '../../../templates/types/oidc-provider';
|
||||
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import type { Json } from '../../util/Json';
|
||||
import type { Interaction } from './InteractionHandler';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
const logger = getLoggerFor('AccountUtil');
|
||||
|
||||
/**
|
||||
* A JSON object.
|
||||
*/
|
||||
export type Json = string | number | boolean | Dict<Json> | Json[];
|
||||
|
||||
/**
|
||||
* Contains a JSON object and any associated metadata.
|
||||
* Similar to a {@link Representation} but with all the data in memory instead of as a stream
|
||||
@ -53,6 +49,7 @@ export type AccountInteractionResults = { [ACCOUNT_PROMPT]?: string } & Interact
|
||||
|
||||
/**
|
||||
* Updates the `oidcInteraction` object with the necessary data in case a prompt gets updated.
|
||||
*
|
||||
* @param oidcInteraction - Interaction to update.
|
||||
* @param result - New data to add to the interaction.
|
||||
* @param mergeWithLastSubmission - If this new data needs to be merged with already existing data in the interaction.
|
||||
@ -76,6 +73,7 @@ export async function finishInteraction(
|
||||
* Removes the WebID, the `accountId`, from the OIDC session object,
|
||||
* allowing us to replace it with a new value.
|
||||
* If there is no session in the Interaction, nothing will happen.
|
||||
*
|
||||
* @param provider - The OIDC provider.
|
||||
* @param oidcInteraction - The current interaction.
|
||||
*/
|
||||
|
@ -3,10 +3,10 @@ import type { Representation } from '../../http/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||
import { APPLICATION_JSON } from '../../util/ContentTypes';
|
||||
import type { Json } from '../../util/Json';
|
||||
import { readJsonStream } from '../../util/StreamUtil';
|
||||
import type { InteractionHandlerInput } from './InteractionHandler';
|
||||
import { InteractionHandler } from './InteractionHandler';
|
||||
import type { Json } from './InteractionUtil';
|
||||
import type { JsonInteractionHandler, JsonInteractionHandlerInput } from './JsonInteractionHandler';
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,9 @@
|
||||
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||
import type { Json } from '../../util/Json';
|
||||
import type { Interaction } from './InteractionHandler';
|
||||
import type { Json, JsonRepresentation } from './InteractionUtil';
|
||||
import type { JsonRepresentation } from './InteractionUtil';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
export interface JsonInteractionHandlerInput {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Json } from '../../util/Json';
|
||||
import { ControlHandler } from './ControlHandler';
|
||||
import type { Json } from './InteractionUtil';
|
||||
import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { Json, JsonRepresentation } from './InteractionUtil';
|
||||
import type { Json } from '../../util/Json';
|
||||
import type { JsonRepresentation } from './InteractionUtil';
|
||||
import { JsonInteractionHandler } from './JsonInteractionHandler';
|
||||
|
||||
/**
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { string } from 'yup';
|
||||
import type { ObjectSchema, Schema, ValidateOptions } from 'yup';
|
||||
import type { AnyObject, Maybe, ObjectSchema, Schema, ValidateOptions } from 'yup';
|
||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||
import type { Json } from '../../util/Json';
|
||||
import { isUrl } from '../../util/StringUtil';
|
||||
import type { Json } from './InteractionUtil';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
type BaseObjectSchema = ObjectSchema<Maybe<AnyObject>>;
|
||||
|
||||
// The builtin `url` validator of `yup` does not support localhost URLs, so we create a custom one here.
|
||||
// The reason for having a URL validator on the WebID is to prevent us from generating invalid ACL,
|
||||
// which would break the pod creation causing us to have an incomplete pod.
|
||||
// We validate the WebID URL to prevent generation of invalid ACL,
|
||||
// which would break the pod creation, causing us to have an incomplete pod.
|
||||
export const URL_SCHEMA = string().trim().optional().test({
|
||||
name: 'url',
|
||||
message: (value: { value: string }): string => `"${value.value}" is not a valid URL`,
|
||||
@ -20,16 +22,16 @@ export const URL_SCHEMA = string().trim().optional().test({
|
||||
},
|
||||
});
|
||||
|
||||
function isObjectSchema(schema: Schema): schema is ObjectSchema<any> {
|
||||
function isObjectSchema(schema: Schema): schema is BaseObjectSchema {
|
||||
return schema.type === 'object';
|
||||
}
|
||||
|
||||
// `T` can't extend Schema since it could also be a Reference, which is a type `yup` doesn't export
|
||||
type SchemaType<T> = T extends ObjectSchema<any> ? ObjectType<T> : { required: boolean; type: string };
|
||||
type SchemaType<T> = T extends BaseObjectSchema ? ObjectType<T> : { required: boolean; type: string };
|
||||
// The type of the fields in an object schema
|
||||
type FieldType<T extends ObjectSchema<any>> = T extends { fields: Record<infer R, any> } ? R : never;
|
||||
type FieldType<T extends BaseObjectSchema> = T extends { fields: Record<infer R, unknown> } ? R : never;
|
||||
// Simplified type we use to represent yup objects
|
||||
type ObjectType<T extends ObjectSchema<any>> =
|
||||
type ObjectType<T extends BaseObjectSchema> =
|
||||
{ required: boolean; type: 'object'; fields: {[ K in FieldType<T> ]: SchemaType<T['fields'][K]> }};
|
||||
|
||||
/**
|
||||
@ -50,7 +52,7 @@ function parseSchemaDescription<T extends Schema>(schema: T): SchemaType<T> {
|
||||
/**
|
||||
* Generates a simplified representation of a yup schema.
|
||||
*/
|
||||
export function parseSchema<T extends ObjectSchema<any>>(schema: T): Pick<SchemaType<T>, 'fields'> {
|
||||
export function parseSchema<T extends BaseObjectSchema>(schema: T): Pick<SchemaType<T>, 'fields'> {
|
||||
const result = parseSchemaDescription(schema);
|
||||
return { fields: result.fields };
|
||||
}
|
||||
@ -58,13 +60,12 @@ export function parseSchema<T extends ObjectSchema<any>>(schema: T): Pick<Schema
|
||||
/**
|
||||
* Same functionality as the yup validate function, but throws a {@link BadRequestHttpError} if there is an error.
|
||||
*/
|
||||
export async function validateWithError<T extends ObjectSchema<any>>(
|
||||
export async function validateWithError<T extends BaseObjectSchema>(
|
||||
schema: T,
|
||||
data: unknown,
|
||||
options?: ValidateOptions<any>,
|
||||
options?: ValidateOptions<AnyObject>,
|
||||
): Promise<T['__outputType']> {
|
||||
try {
|
||||
// eslint-disable-next-line ts/no-unsafe-return
|
||||
return await schema.validate(data, options);
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestHttpError(createErrorMessage(error));
|
||||
|
@ -5,7 +5,6 @@ export const ACCOUNT_SETTINGS_REMEMBER_LOGIN = 'rememberLogin';
|
||||
|
||||
export type AccountSettings = { [ACCOUNT_SETTINGS_REMEMBER_LOGIN]?: boolean };
|
||||
|
||||
/* eslint-disable ts/method-signature-style */
|
||||
/**
|
||||
* Used to store account data.
|
||||
*/
|
||||
@ -20,16 +19,18 @@ export interface AccountStore {
|
||||
|
||||
/**
|
||||
* Finds the setting of the account with the given identifier.
|
||||
*
|
||||
* @param id - The account identifier.
|
||||
* @param setting - The setting to find the value of.
|
||||
*/
|
||||
getSetting<T extends keyof AccountSettings>(id: string, setting: T): Promise<AccountSettings[T]>;
|
||||
getSetting: <T extends keyof AccountSettings>(id: string, setting: T) => Promise<AccountSettings[T]>;
|
||||
|
||||
/**
|
||||
* Updates the settings for the account with the given identifier to the new values.
|
||||
*
|
||||
* @param id - The account identifier.
|
||||
* @param setting - The setting to update.
|
||||
* @param value - The new value for the setting.
|
||||
*/
|
||||
updateSetting<T extends keyof AccountSettings>(id: string, setting: T, value: AccountSettings[T]): Promise<void>;
|
||||
updateSetting: <T extends keyof AccountSettings>(id: string, setting: T, value: AccountSettings[T]) => Promise<void>;
|
||||
}
|
||||
|
@ -22,9 +22,10 @@ export class BaseAccountStore extends Initializer implements AccountStore {
|
||||
private readonly storage: AccountLoginStorage<{ [ACCOUNT_TYPE]: typeof ACCOUNT_STORAGE_DESCRIPTION }>;
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>) {
|
||||
// Wrong typings to prevent Components.js typing issues
|
||||
public constructor(storage: AccountLoginStorage<Record<string, never>>) {
|
||||
super();
|
||||
this.storage = storage as typeof this.storage;
|
||||
this.storage = storage as unknown as typeof this.storage;
|
||||
}
|
||||
|
||||
// Initialize the type definitions
|
||||
|
@ -23,7 +23,7 @@ export class BaseCookieStore implements CookieStore {
|
||||
}
|
||||
|
||||
public async get(cookie: string): Promise<string | undefined> {
|
||||
return await this.storage.get(cookie);
|
||||
return this.storage.get(cookie);
|
||||
}
|
||||
|
||||
public async refresh(cookie: string): Promise<Date | undefined> {
|
||||
|
@ -61,7 +61,7 @@ export class BaseLoginAccountStorage<T extends IndexTypeCollection<T>> implement
|
||||
return this.storage.defineType(type, description);
|
||||
}
|
||||
|
||||
public async createIndex<TType extends StringKey<T>>(type: TType, key: StringKey<TType>): Promise<void> {
|
||||
public async createIndex<TType extends StringKey<T>>(type: TType, key: StringKey<T[TType]>): Promise<void> {
|
||||
return this.storage.createIndex(type, key);
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ export class BaseLoginAccountStorage<T extends IndexTypeCollection<T>> implement
|
||||
}
|
||||
|
||||
public async findIds<TType extends StringKey<T>>(type: TType, query: IndexedQuery<T, TType>): Promise<string[]> {
|
||||
return await this.storage.findIds(type, query);
|
||||
return this.storage.findIds(type, query);
|
||||
}
|
||||
|
||||
public async set<TType extends StringKey<T>>(type: TType, value: TypeObject<T[TType]>): Promise<void> {
|
||||
|
@ -5,6 +5,7 @@ export interface CookieStore {
|
||||
/**
|
||||
* Generates and stores a new cookie for the given accountId.
|
||||
* This does not replace previously generated cookies.
|
||||
*
|
||||
* @param accountId - Account to create a cookie for.
|
||||
*
|
||||
* @returns The generated cookie.
|
||||
@ -13,18 +14,21 @@ export interface CookieStore {
|
||||
|
||||
/**
|
||||
* Return the accountID associated with the given cookie.
|
||||
*
|
||||
* @param cookie - Cookie to find the account for.
|
||||
*/
|
||||
get: (cookie: string) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Refreshes the cookie expiration and returns when it will expire if the cookie exists.
|
||||
*
|
||||
* @param cookie - Cookie to refresh.
|
||||
*/
|
||||
refresh: (cookie: string) => Promise<Date | undefined>;
|
||||
|
||||
/**
|
||||
* Deletes the given cookie.
|
||||
*
|
||||
* @param cookie - Cookie to delete.
|
||||
*/
|
||||
delete: (cookie: string) => Promise<boolean>;
|
||||
|
@ -27,9 +27,10 @@ export class BaseClientCredentialsStore extends Initializer implements ClientCre
|
||||
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>) {
|
||||
// Wrong typings to prevent Components.js typing issues
|
||||
public constructor(storage: AccountLoginStorage<Record<string, never>>) {
|
||||
super();
|
||||
this.storage = storage as typeof this.storage;
|
||||
this.storage = storage as unknown as typeof this.storage;
|
||||
}
|
||||
|
||||
// Initialize the type definitions
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { RepresentationMetadata } from '../../../http/representation/RepresentationMetadata';
|
||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||
import type { Json } from '../../../util/Json';
|
||||
import { SOLID_HTTP } from '../../../util/Vocabularies';
|
||||
import { ACCOUNT_SETTINGS_REMEMBER_LOGIN } from '../account/util/AccountStore';
|
||||
import type { AccountStore } from '../account/util/AccountStore';
|
||||
import type { CookieStore } from '../account/util/CookieStore';
|
||||
import type { Json, JsonRepresentation } from '../InteractionUtil';
|
||||
import type { JsonRepresentation } from '../InteractionUtil';
|
||||
import { finishInteraction } from '../InteractionUtil';
|
||||
import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
|
||||
import { JsonInteractionHandler } from '../JsonInteractionHandler';
|
||||
@ -80,7 +81,8 @@ export abstract class ResolveLoginHandler extends JsonInteractionHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the account setting that determines if the login status needs to be remembered.
|
||||
* Updates the account setting that determines whether the login status needs to be remembered.
|
||||
*
|
||||
* @param accountId - ID of the account.
|
||||
* @param remember - If the account should be remembered or not. The setting will not be updated if this is undefined.
|
||||
*/
|
||||
@ -94,6 +96,7 @@ export abstract class ResolveLoginHandler extends JsonInteractionHandler {
|
||||
|
||||
/**
|
||||
* Takes the necessary steps to log a user in.
|
||||
*
|
||||
* @param input - Same input that was passed to the handle function.
|
||||
*/
|
||||
public abstract login(input: JsonInteractionHandlerInput): Promise<JsonRepresentation<LoginOutputType>>;
|
||||
|
@ -29,9 +29,10 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
private readonly saltRounds: number;
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>, saltRounds = 10) {
|
||||
// Wrong typings to prevent Components.js typing issues
|
||||
public constructor(storage: AccountLoginStorage<Record<string, never>>, saltRounds = 10) {
|
||||
super();
|
||||
this.storage = storage as typeof this.storage;
|
||||
this.storage = storage as unknown as typeof this.storage;
|
||||
this.saltRounds = saltRounds;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,9 @@ export interface ForgotPasswordStore {
|
||||
* Creates a Forgot Password Confirmation Record. This will be to remember that
|
||||
* a user has made a request to reset a password. Throws an error if the email doesn't
|
||||
* exist.
|
||||
*
|
||||
* @param id - ID of the email/password login object.
|
||||
*
|
||||
* @returns The record id. This should be included in the reset password link.
|
||||
*/
|
||||
generate: (id: string) => Promise<string>;
|
||||
@ -14,13 +16,16 @@ export interface ForgotPasswordStore {
|
||||
/**
|
||||
* Gets the email associated with the forgot password confirmation record
|
||||
* or undefined if it's not present.
|
||||
*
|
||||
* @param recordId - The record id retrieved from the link.
|
||||
*
|
||||
* @returns The user's email.
|
||||
*/
|
||||
get: (recordId: string) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Deletes the Forgot Password Confirmation Record.
|
||||
*
|
||||
* @param recordId - The record id of the forgot password confirmation record.
|
||||
*/
|
||||
delete: (recordId: string) => Promise<boolean>;
|
||||
|
@ -44,9 +44,10 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>, manager: PodManager, visible = false) {
|
||||
// Wrong typings to prevent Components.js typing issues
|
||||
public constructor(storage: AccountLoginStorage<Record<string, never>>, manager: PodManager, visible = false) {
|
||||
super();
|
||||
this.storage = storage as typeof this.storage;
|
||||
this.storage = storage as unknown as typeof this.storage;
|
||||
this.visible = visible;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import type { InteractionRoute } from './InteractionRoute';
|
||||
* Rejects operations that target a different route,
|
||||
* otherwise the input parameters are passed to the source handler.
|
||||
*/
|
||||
export class InteractionRouteHandler<T extends InteractionRoute<any>> extends JsonInteractionHandler {
|
||||
export class InteractionRouteHandler<T extends InteractionRoute<string>> extends JsonInteractionHandler {
|
||||
protected readonly route: T;
|
||||
protected readonly source: JsonInteractionHandler;
|
||||
|
||||
|
@ -23,9 +23,10 @@ export class BaseWebIdStore extends Initializer implements WebIdStore {
|
||||
private readonly storage: AccountLoginStorage<{ [WEBID_STORAGE_TYPE]: typeof WEBID_STORAGE_DESCRIPTION }>;
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>) {
|
||||
// Wrong typings to prevent Components.js typing issues
|
||||
public constructor(storage: AccountLoginStorage<Record<string, never>>) {
|
||||
super();
|
||||
this.storage = storage as typeof this.storage;
|
||||
this.storage = storage as unknown as typeof this.storage;
|
||||
}
|
||||
|
||||
// Initialize the type definitions
|
||||
|
@ -81,6 +81,7 @@ export class ClientIdAdapter extends PassthroughAdapter {
|
||||
|
||||
/**
|
||||
* Parses RDF data found at a Client ID.
|
||||
*
|
||||
* @param data - Raw data from the Client ID.
|
||||
* @param id - The actual Client ID.
|
||||
* @param response - Response object from the request.
|
||||
|
10
src/index.ts
10
src/index.ts
@ -100,6 +100,7 @@ export * from './http/output/error/ConvertingErrorHandler';
|
||||
export * from './http/output/error/ErrorHandler';
|
||||
export * from './http/output/error/RedirectingErrorHandler';
|
||||
export * from './http/output/error/SafeErrorHandler';
|
||||
export * from './http/output/error/TargetExtractorErrorHandler';
|
||||
|
||||
// HTTP/Output/Metadata
|
||||
export * from './http/output/metadata/AllowAcceptHeaderWriter';
|
||||
@ -403,6 +404,14 @@ export * from './server/notifications/WebSocketChannel2023/WebSocket2023Util';
|
||||
export * from './server/notifications/WebSocketChannel2023/WebSocketMap';
|
||||
export * from './server/notifications/WebSocketChannel2023/WebSocketChannel2023Type';
|
||||
|
||||
// Server/Notifications/StreamingHTTPChannel2023
|
||||
export * from './server/notifications/StreamingHttpChannel2023/StreamingHttp2023Emitter';
|
||||
export * from './server/notifications/StreamingHttpChannel2023/StreamingHttp2023Util';
|
||||
export * from './server/notifications/StreamingHttpChannel2023/StreamingHttpListeningActivityHandler';
|
||||
export * from './server/notifications/StreamingHttpChannel2023/StreamingHttpMap';
|
||||
export * from './server/notifications/StreamingHttpChannel2023/StreamingHttpMetadataWriter';
|
||||
export * from './server/notifications/StreamingHttpChannel2023/StreamingHttpRequestHandler';
|
||||
|
||||
// Server/Notifications
|
||||
export * from './server/notifications/ActivityEmitter';
|
||||
export * from './server/notifications/BaseChannelType';
|
||||
@ -615,6 +624,7 @@ export * from './util/GenericEventEmitter';
|
||||
export * from './util/GuardedStream';
|
||||
export * from './util/HeaderUtil';
|
||||
export * from './util/IterableUtil';
|
||||
export * from './util/Json';
|
||||
export * from './util/PathUtil';
|
||||
export * from './util/PromiseUtil';
|
||||
export * from './util/QuadUtil';
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable unicorn/no-process-exit */
|
||||
import { existsSync } from 'node:fs';
|
||||
import type { WriteStream } from 'node:tty';
|
||||
import type { IComponentsManagerBuilderOptions } from 'componentsjs';
|
||||
@ -105,9 +104,9 @@ export class AppRunner {
|
||||
let configs = input.config ?? [ '@css:config/default.json' ];
|
||||
configs = (Array.isArray(configs) ? configs : [ configs ]).map(resolveAssetPath);
|
||||
|
||||
let componentsManager: ComponentsManager<any>;
|
||||
let componentsManager: ComponentsManager<App | CliResolver>;
|
||||
try {
|
||||
componentsManager = await this.createComponentsManager<any>(loaderProperties, configs);
|
||||
componentsManager = await this.createComponentsManager<App>(loaderProperties, configs);
|
||||
} catch (error: unknown) {
|
||||
this.resolveError(`Could not build the config files from ${configs.join(',')}`, error);
|
||||
}
|
||||
@ -145,6 +144,7 @@ export class AppRunner {
|
||||
public runCliSync({ argv, stderr = process.stderr }: { argv?: CliArgv; stderr?: WriteStream }): void {
|
||||
this.runCli(argv).catch((error): never => {
|
||||
stderr.write(createErrorMessage(error));
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@ -203,6 +203,7 @@ export class AppRunner {
|
||||
/**
|
||||
* Retrieves settings from package.json or configuration file when
|
||||
* part of an npm project.
|
||||
*
|
||||
* @returns The settings defined in the configuration file
|
||||
*/
|
||||
public async getPackageSettings(): Promise<undefined | Record<string, unknown>> {
|
||||
@ -225,9 +226,9 @@ export class AppRunner {
|
||||
return import(cssConfigPathJs) as Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
// Finally try and read from the config.community-solid-server
|
||||
// Finally try to read from the config.community-solid-server
|
||||
// field in the root package.json
|
||||
const pkg = await readJSON(packageJsonPath) as { config?: Record<string, any> };
|
||||
const pkg = await readJSON(packageJsonPath) as { config?: Record<string, unknown> };
|
||||
if (typeof pkg.config?.['community-solid-server'] === 'object') {
|
||||
return pkg.config['community-solid-server'] as Record<string, unknown>;
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import { Initializer } from './Initializer';
|
||||
* Part of the dynamic pod creation.
|
||||
* Reads the contents from the configuration storage, uses those values to instantiate ResourceStores,
|
||||
* and then adds them to the routing storage.
|
||||
*
|
||||
* @see {@link ConfigPodManager}, {@link TemplatedPodGenerator}, {@link BaseUrlRouterRule}
|
||||
*/
|
||||
export class ConfigPodInitializer extends Initializer {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Server } from 'node:http';
|
||||
import { URL } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { isHttpsServer } from '../server/HttpServerFactory';
|
||||
|
@ -19,7 +19,7 @@ export class YargsParameter {
|
||||
* @param name - Name of the parameter. Corresponds to the first parameter passed to the `yargs.options` function.
|
||||
* @param options - Options for a single parameter that should be parsed. @range {json}
|
||||
*/
|
||||
public constructor(name: string, options: Record<string, any>) {
|
||||
public constructor(name: string, options: Record<string, unknown>) {
|
||||
this.name = name;
|
||||
this.options = options;
|
||||
}
|
||||
@ -76,8 +76,9 @@ export class YargsCliExtractor extends CliExtractor {
|
||||
yArgv.check((args): boolean => {
|
||||
for (const [ name, options ] of Object.entries(this.yargsArgOptions)) {
|
||||
if (options.type !== 'array' && Array.isArray(args[name])) {
|
||||
// eslint-disable-next-line ts/restrict-template-expressions
|
||||
throw new Error(`Multiple values for --${name} (-${options.alias}) were provided where only one is allowed`);
|
||||
throw new Error(
|
||||
`Multiple values for --${name} (-${options.alias as string}) were provided where only one is allowed`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -18,7 +18,9 @@ enum ClusterMode {
|
||||
|
||||
/**
|
||||
* Convert workers amount to {@link ClusterMode}
|
||||
*
|
||||
* @param workers - Amount of workers
|
||||
*
|
||||
* @returns ClusterMode enum value
|
||||
*/
|
||||
function toClusterMode(workers: number): ClusterMode {
|
||||
@ -92,6 +94,7 @@ export class ClusterManager {
|
||||
|
||||
/**
|
||||
* Check whether the CSS server was booted in single threaded mode.
|
||||
*
|
||||
* @returns True is single threaded.
|
||||
*/
|
||||
public isSingleThreaded(): boolean {
|
||||
@ -100,6 +103,7 @@ export class ClusterManager {
|
||||
|
||||
/**
|
||||
* Whether the calling process is the primary process.
|
||||
*
|
||||
* @returns True if primary
|
||||
*/
|
||||
public isPrimary(): boolean {
|
||||
@ -108,6 +112,7 @@ export class ClusterManager {
|
||||
|
||||
/**
|
||||
* Whether the calling process is a worker process.
|
||||
*
|
||||
* @returns True if worker
|
||||
*/
|
||||
public isWorker(): boolean {
|
||||
|
@ -11,8 +11,10 @@ export interface SingleThreaded {}
|
||||
|
||||
/**
|
||||
* Convert an exported interface name to the properly expected Components.js type URI.
|
||||
*
|
||||
* @param componentsManager - The currently used ComponentsManager
|
||||
* @param interfaceName - An interface name
|
||||
*
|
||||
* @returns A Components.js type URI
|
||||
*/
|
||||
export async function toComponentsJsType<T>(componentsManager: ComponentsManager<T>, interfaceName: string):
|
||||
@ -38,6 +40,7 @@ Promise<string> {
|
||||
/**
|
||||
* Will list class names of components instantiated implementing the {@link SingleThreaded}
|
||||
* interface while the application is being run in multithreaded mode.
|
||||
*
|
||||
* @param componentsManager - The componentsManager being used to set up the application
|
||||
*/
|
||||
export async function listSingleThreadedComponents<T>(componentsManager: ComponentsManager<T>): Promise<string[]> {
|
||||
|
@ -68,11 +68,13 @@ export interface V6MigrationInitializerArgs {
|
||||
/**
|
||||
* Storages for which all entries need to be removed.
|
||||
*/
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
cleanupStorages: KeyValueStorage<string, any>[];
|
||||
/**
|
||||
* The storage that will contain the account data in the new format.
|
||||
* Wrong typings to prevent Components.js typing issues.
|
||||
*/
|
||||
newAccountStorage: AccountLoginStorage<any>;
|
||||
newAccountStorage: AccountLoginStorage<Record<string, never>>;
|
||||
/**
|
||||
* The storage that will contain the setup entries in the new format.
|
||||
*/
|
||||
@ -100,7 +102,7 @@ export class V6MigrationInitializer extends Initializer {
|
||||
|
||||
private readonly accountStorage: KeyValueStorage<string, Account | Settings>;
|
||||
private readonly clientCredentialsStorage: KeyValueStorage<string, ClientCredentials>;
|
||||
private readonly cleanupStorages: KeyValueStorage<string, any>[];
|
||||
private readonly cleanupStorages: KeyValueStorage<string, unknown>[];
|
||||
|
||||
private readonly newAccountStorage: AccountLoginStorage<typeof STORAGE_DESCRIPTION>;
|
||||
private readonly newSetupStorage: KeyValueStorage<string, string>;
|
||||
@ -113,7 +115,7 @@ export class V6MigrationInitializer extends Initializer {
|
||||
this.accountStorage = args.accountStorage;
|
||||
this.clientCredentialsStorage = args.clientCredentialsStorage;
|
||||
this.cleanupStorages = args.cleanupStorages;
|
||||
this.newAccountStorage = args.newAccountStorage as AccountLoginStorage<typeof STORAGE_DESCRIPTION>;
|
||||
this.newAccountStorage = args.newAccountStorage as unknown as AccountLoginStorage<typeof STORAGE_DESCRIPTION>;
|
||||
this.newSetupStorage = args.newSetupStorage;
|
||||
}
|
||||
|
||||
@ -147,7 +149,7 @@ export class V6MigrationInitializer extends Initializer {
|
||||
].join(' '), resolve);
|
||||
});
|
||||
readline.close();
|
||||
if (!/^y(?:es)?$/ui.test(answer)) {
|
||||
if (!/^y(?:es)?$/iu.test(answer)) {
|
||||
throw new Error('Stopping server as migration was cancelled.');
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export class CombinedShorthandResolver extends ShorthandResolver {
|
||||
}
|
||||
|
||||
public async handle(input: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const vars: Record<string, any> = {};
|
||||
const vars: Record<string, unknown> = {};
|
||||
for (const [ name, computer ] of Object.entries(this.resolvers)) {
|
||||
try {
|
||||
vars[name] = await computer.handleSafe(input);
|
||||
|
@ -42,6 +42,7 @@ export function getLoggerFor(loggable: string | Instance): Logger {
|
||||
/**
|
||||
* Sets the global logger factory.
|
||||
* This causes loggers created by {@link getLoggerFor} to delegate to a logger from the given factory.
|
||||
*
|
||||
* @param loggerFactory - A logger factory.
|
||||
*/
|
||||
export function setGlobalLoggerFactory(loggerFactory: LoggerFactory): void {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import cluster from 'node:cluster';
|
||||
import process from 'node:process';
|
||||
import type { LogLevel } from './LogLevel';
|
||||
|
||||
export interface LogMetadata {
|
||||
@ -18,6 +17,7 @@ export interface SimpleLogger {
|
||||
/**
|
||||
* Log the given message at the given level.
|
||||
* If the internal level is higher than the given level, the message may be voided.
|
||||
*
|
||||
* @param level - The level to log at.
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
@ -34,6 +34,7 @@ export interface Logger extends SimpleLogger {
|
||||
/**
|
||||
* Log the given message at the given level.
|
||||
* If the internal level is higher than the given level, the message may be voided.
|
||||
*
|
||||
* @param level - The level to log at.
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
@ -42,6 +43,7 @@ export interface Logger extends SimpleLogger {
|
||||
|
||||
/**
|
||||
* Log a message at the 'error' level.
|
||||
*
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
*/
|
||||
@ -49,6 +51,7 @@ export interface Logger extends SimpleLogger {
|
||||
|
||||
/**
|
||||
* Log a message at the 'warn' level.
|
||||
*
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
*/
|
||||
@ -56,6 +59,7 @@ export interface Logger extends SimpleLogger {
|
||||
|
||||
/**
|
||||
* Log a message at the 'info' level.
|
||||
*
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
*/
|
||||
@ -63,6 +67,7 @@ export interface Logger extends SimpleLogger {
|
||||
|
||||
/**
|
||||
* Log a message at the 'verbose' level.
|
||||
*
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
*/
|
||||
@ -70,6 +75,7 @@ export interface Logger extends SimpleLogger {
|
||||
|
||||
/**
|
||||
* Log a message at the 'debug' level.
|
||||
*
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
*/
|
||||
@ -77,6 +83,7 @@ export interface Logger extends SimpleLogger {
|
||||
|
||||
/**
|
||||
* Log a message at the 'silly' level.
|
||||
*
|
||||
* @param message - The message to log.
|
||||
* @param meta - Optional metadata to include in the log message.
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@ import type { Logger } from './Logger';
|
||||
export interface LoggerFactory {
|
||||
/**
|
||||
* Create a logger instance for the given label.
|
||||
*
|
||||
* @param label - A label that is used to identify the given logger.
|
||||
*/
|
||||
createLogger: (label: string) => Logger;
|
||||
|
@ -13,7 +13,7 @@ export class WinstonLogger extends BaseLogger {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public log(level: LogLevel, message: string, meta?: any): this {
|
||||
public log(level: LogLevel, message: string, meta?: unknown): this {
|
||||
this.logger.log(level, message, meta);
|
||||
return this;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { TransformableInfo } from 'logform';
|
||||
import { createLogger, format, transports } from 'winston';
|
||||
import type * as Transport from 'winston-transport';
|
||||
import type { Logger, LogMetadata } from './Logger';
|
||||
@ -33,7 +34,7 @@ export class WinstonLoggerFactory implements LoggerFactory {
|
||||
format.timestamp(),
|
||||
format.metadata({ fillExcept: [ 'level', 'timestamp', 'label', 'message' ]}),
|
||||
format.printf(
|
||||
({ level: levelInner, message, label: labelInner, timestamp, metadata: meta }: Record<string, any>): string =>
|
||||
({ level: levelInner, message, label: labelInner, timestamp, metadata: meta }: TransformableInfo): string =>
|
||||
`${timestamp} [${labelInner}] {${this.clusterInfo(meta as LogMetadata)}} ${levelInner}: ${message}`,
|
||||
),
|
||||
),
|
||||
|
@ -7,6 +7,7 @@ import type { PodSettings } from './settings/PodSettings';
|
||||
export interface PodManager {
|
||||
/**
|
||||
* Creates a pod for the given settings.
|
||||
*
|
||||
* @param settings - Settings describing the pod.
|
||||
* @param overwrite - If the creation should proceed if there already is a resource there.
|
||||
*/
|
||||
|
@ -10,7 +10,7 @@ import type { ComponentsJsFactory } from './ComponentsJsFactory';
|
||||
* but moduleState will be stored in between calls.
|
||||
*/
|
||||
export class BaseComponentsJsFactory implements ComponentsJsFactory {
|
||||
private readonly options: IComponentsManagerBuilderOptions<any>;
|
||||
private readonly options: IComponentsManagerBuilderOptions<unknown>;
|
||||
|
||||
public constructor(relativeModulePath = '../../../', logLevel = 'error') {
|
||||
this.options = {
|
||||
@ -21,7 +21,7 @@ export class BaseComponentsJsFactory implements ComponentsJsFactory {
|
||||
};
|
||||
}
|
||||
|
||||
private async buildManager(): Promise<ComponentsManager<any>> {
|
||||
private async buildManager(): Promise<ComponentsManager<unknown>> {
|
||||
const manager = await ComponentsManager.build(this.options);
|
||||
this.options.moduleState = manager.moduleState;
|
||||
return manager;
|
||||
@ -29,16 +29,17 @@ export class BaseComponentsJsFactory implements ComponentsJsFactory {
|
||||
|
||||
/**
|
||||
* Calls Components.js to instantiate a new object.
|
||||
*
|
||||
* @param configPath - Location of the config to instantiate.
|
||||
* @param componentIri - Iri of the object in the config that will be the result.
|
||||
* @param variables - Variables to send to Components.js
|
||||
*
|
||||
* @returns The resulting object, corresponding to the given component IRI.
|
||||
*/
|
||||
public async generate<T>(configPath: string, componentIri: string, variables: Record<string, any>):
|
||||
public async generate<T>(configPath: string, componentIri: string, variables: Record<string, unknown>):
|
||||
Promise<T> {
|
||||
const manager = await this.buildManager();
|
||||
await manager.configRegistry.register(configPath);
|
||||
return await manager.instantiate(componentIri, { variables });
|
||||
return manager.instantiate(componentIri, { variables });
|
||||
}
|
||||
}
|
||||
|
@ -185,8 +185,20 @@ export class BaseResourcesGenerator implements TemplatedResourcesGenerator {
|
||||
data = await this.processFile(link, options);
|
||||
metadata.contentType = link.contentType;
|
||||
}
|
||||
// Do not yield a container resource if it already exists
|
||||
if (!isContainerIdentifier(link.identifier) || !await this.store.hasResource(link.identifier)) {
|
||||
|
||||
// Add metadata from .meta file if there is one
|
||||
if (metaLink) {
|
||||
const rawMetadata = await this.generateMetadata(metaLink, options);
|
||||
if (rawMetadata.contentType) {
|
||||
// Prevent having 2 content types
|
||||
metadata.contentType = undefined;
|
||||
}
|
||||
metadata.setMetadata(rawMetadata);
|
||||
this.logger.debug(`Adding metadata for ${metaLink.identifier.path}`);
|
||||
}
|
||||
|
||||
const shouldYield = !isContainerIdentifier(link.identifier) || !await this.store.hasResource(link.identifier);
|
||||
if (shouldYield) {
|
||||
this.logger.debug(`Generating resource ${link.identifier.path}`);
|
||||
yield {
|
||||
identifier: link.identifier,
|
||||
@ -194,20 +206,15 @@ export class BaseResourcesGenerator implements TemplatedResourcesGenerator {
|
||||
};
|
||||
}
|
||||
|
||||
// Add metadata from .meta file if there is one
|
||||
if (metaLink) {
|
||||
const rawMetadata = await this.generateMetadata(metaLink, options);
|
||||
if (!rawMetadata.contentType) {
|
||||
// Make sure this does not remove the content-type if none is explicitly defined
|
||||
rawMetadata.contentType = metadata.contentType;
|
||||
}
|
||||
// Still need to yield metadata in case the actual resource is not being yielded.
|
||||
// We also do this for containers as existing containers can't be edited in the same way.
|
||||
if (metaLink && (!shouldYield || isContainerIdentifier(link.identifier))) {
|
||||
const metaIdentifier = this.metadataStrategy.getAuxiliaryIdentifier(link.identifier);
|
||||
const descriptionMeta = new RepresentationMetadata(metaIdentifier);
|
||||
addResourceMetadata(rawMetadata, isContainerIdentifier(link.identifier));
|
||||
addResourceMetadata(metadata, isContainerIdentifier(link.identifier));
|
||||
this.logger.debug(`Generating resource ${metaIdentifier.path}`);
|
||||
yield {
|
||||
identifier: metaIdentifier,
|
||||
representation: new BasicRepresentation(rawMetadata.quads(), descriptionMeta, INTERNAL_QUADS),
|
||||
representation: new BasicRepresentation(metadata.quads(), metaIdentifier, INTERNAL_QUADS),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,12 @@
|
||||
export interface ComponentsJsFactory {
|
||||
/**
|
||||
* Instantiates a new object using Components.js.
|
||||
*
|
||||
* @param configPath - Location of the config to instantiate.
|
||||
* @param componentIri - Iri of the object in the config that will be the result.
|
||||
* @param componentIri - IRI of the object in the config that will be the result.
|
||||
* @param variables - Variables to send to Components.js
|
||||
*
|
||||
* @returns The resulting object, corresponding to the given component IRI.
|
||||
*/
|
||||
generate: <T>(configPath: string, componentIri: string, variables: Record<string, any>) => Promise<T>;
|
||||
generate: <T>(configPath: string, componentIri: string, variables: Record<string, unknown>) => Promise<T>;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import type { ResourcesGenerator } from './ResourcesGenerator';
|
||||
|
||||
/**
|
||||
* Generates resources with the given generator and adds them to the given store.
|
||||
*
|
||||
* @param settings - Settings from which the pod is being created.
|
||||
* @param generator - Generator to be used.
|
||||
* @param store - Store to be updated.
|
||||
|
@ -15,6 +15,7 @@ export interface ResourcesGenerator {
|
||||
/**
|
||||
* Generates resources with the given options.
|
||||
* The output Iterable should be sorted so that containers always appear before their contents.
|
||||
*
|
||||
* @param location - Base identifier.
|
||||
* @param options - Options that can be used when generating resources.
|
||||
*
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user