Merge branch 'refs/heads/main' into versions/next-major

# Conflicts:
#	package-lock.json
#	package.json
This commit is contained in:
Joachim Van Herwegen 2024-09-06 09:52:26 +02:00
commit 3902eccbde
266 changed files with 12581 additions and 9365 deletions

View File

@ -42,7 +42,7 @@ jobs:
with: with:
node-version: 16.x node-version: 16.x
- name: Check out the project - name: Check out the project
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.7
with: with:
ref: ${{ inputs.branch || github.ref }} ref: ${{ inputs.branch || github.ref }}
- name: Install dependencies and run build scripts - name: Install dependencies and run build scripts

View File

@ -21,7 +21,7 @@ jobs:
tags: ${{ steps.meta-main.outputs.tags || steps.meta-version.outputs.tags }} tags: ${{ steps.meta-main.outputs.tags || steps.meta-version.outputs.tags }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.7
- if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main') - if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
name: Docker meta edge and version tag name: Docker meta edge and version tag
id: meta-main id: meta-main
@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.7
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
@ -66,7 +66,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and export to docker - name: Build and export to docker
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
load: true load: true
@ -85,7 +85,7 @@ jobs:
done <<< "${{ needs.docker-meta.outputs.tags }}"; done <<< "${{ needs.docker-meta.outputs.tags }}";
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true

View File

@ -21,7 +21,7 @@ jobs:
outputs: outputs:
major: ${{ steps.tagged_version.outputs.major || steps.current_version.outputs.major }} major: ${{ steps.tagged_version.outputs.major || steps.current_version.outputs.major }}
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.7
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 16.x node-version: 16.x
@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: mkdocs-prep needs: mkdocs-prep
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.7
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
@ -62,7 +62,7 @@ jobs:
needs: [mkdocs-prep, mkdocs] needs: [mkdocs-prep, mkdocs]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.7
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 16.x node-version: 16.x
@ -70,7 +70,7 @@ jobs:
- name: Generate typedocs - name: Generate typedocs
run: npm run typedocs run: npm run typedocs
- name: Deploy typedocs - name: Deploy typedocs
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v4
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs publish_dir: ./docs

View File

@ -7,10 +7,10 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.7
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 16.x node-version: 20.x
- run: npm ci --ignore-scripts - run: npm ci --ignore-scripts
- run: npm run lint - run: npm run lint
@ -27,7 +27,8 @@ jobs:
- 18.x - 18.x
- '20.0' - '20.0'
- 20.x - 20.x
- 21.x - '22.1'
- 22.x
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
@ -37,7 +38,7 @@ jobs:
- name: Ensure line endings are consistent - name: Ensure line endings are consistent
run: git config --global core.autocrlf input run: git config --global core.autocrlf input
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.7
- name: Install dependencies and run build scripts - name: Install dependencies and run build scripts
run: npm ci run: npm ci
- name: Type-check tests - name: Type-check tests
@ -59,7 +60,7 @@ jobs:
node-version: node-version:
- 18.x - 18.x
- 20.x - 20.x
- 21.x - 22.x
env: env:
TEST_DOCKER: true TEST_DOCKER: true
services: services:
@ -80,7 +81,7 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.7
- name: Install dependencies and run build scripts - name: Install dependencies and run build scripts
run: npm ci run: npm ci
- name: Run integration tests - name: Run integration tests
@ -94,7 +95,7 @@ jobs:
node-version: node-version:
- 18.x - 18.x
- 20.x - 20.x
- 21.x - 22.x
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
@ -104,7 +105,7 @@ jobs:
- name: Ensure line endings are consistent - name: Ensure line endings are consistent
run: git config --global core.autocrlf input run: git config --global core.autocrlf input
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.7
- name: Install dependencies and run build scripts - name: Install dependencies and run build scripts
run: npm ci run: npm ci
- name: Run integration tests - name: Run integration tests
@ -124,9 +125,9 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 16.x node-version: 20.x
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.7
- name: Install dependencies and run build scripts - name: Install dependencies and run build scripts
run: npm ci run: npm ci
- name: Run deploy tests - name: Run deploy tests

View File

@ -22,7 +22,7 @@ module.exports = {
// Allow multiple subheadings with the same content // Allow multiple subheadings with the same content
// across different section (#1 ##A ##B #2 ##A ##B) // across different section (#1 ##A ##B #2 ##A ##B)
MD024: { MD024: {
allow_different_nesting: true, siblings_only: true,
}, },
// Set Ordered list item prefix to "ordered" (use 1. 2. 3. not 1. 1. 1.) // Set Ordered list item prefix to "ordered" (use 1. 2. 3. not 1. 1. 1.)

2
.nvmrc
View File

@ -1 +1 @@
16 18

View File

@ -3,6 +3,75 @@
All notable changes to this project will be documented in this file. 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
* Allow path segments to start with 2 or more dots ([6fe6b6e](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/6fe6b6ec89cfa3c1005ca4cf2219fc77de3fb975))
* Add priorities to RDF types when converting ([33e9ae4](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/33e9ae41916c9de0638709b02c42936e53d49414))
* Extract root as possible pod when using subdomains ([8fff08a](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/8fff08a9b60a11c7a7f313c540d9f28a2f96ebc0))
* Prevent error when switching accounts ([68975e6](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/68975e6627416c248d82150692199db8a5fd0d31))
* Keep content-type when using metadata templates ([137027e](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/137027e421da9ffa2d2bbc23c08b2a47d4abd328))
## [7.0.4](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.3...v7.0.4) (2024-02-07)
### Chores
* Replace rdf-js import with @rdfjs/types ([e09b53b](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/e09b53b20de0e389715d299466a1e1101579dd07))
### Testing
* Remove workaround for authn library ([7d57359](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/7d573596139283637cf2d1e99d44cb2130268811))
## [7.0.3](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.2...v7.0.3) (2024-01-05) ## [7.0.3](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.2...v7.0.3) (2024-01-05)
### Features ### Features

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright © 20192023 Inrupt Inc. and imec Copyright © 20192024 Inrupt Inc. and imec
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -31,7 +31,9 @@ And, of course, for many others who like to experience Solid.
## ⚡ Running the Community Solid Server ## ⚡ 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 ```shell
npx @solid/community-server 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/ 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 ## 🔧 Configure your server
@ -82,4 +86,6 @@ and available under the [MIT License](https://github.com/CommunitySolidServer/C
Don't hesitate to [start a discussion](https://github.com/CommunitySolidServer/CommunitySolidServer/discussions) Don't hesitate to [start a discussion](https://github.com/CommunitySolidServer/CommunitySolidServer/discussions)
or [report a bug](https://github.com/CommunitySolidServer/CommunitySolidServer/issues). or [report a bug](https://github.com/CommunitySolidServer/CommunitySolidServer/issues).
There's also [a Matrix-based, CSS-focused chat](https://matrix.to/#/#CommunitySolidServer_community:gitter.im)
Learn more about Solid at [solidproject.org](https://solidproject.org/). Learn more about Solid at [solidproject.org](https://solidproject.org/).

View File

@ -1,67 +1,129 @@
# Configuration # Configuration
This folder contains several configurations that can be used to start up the server. 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: All those configurations are created in the same way:
features are enabled or disabled by choosing a specific option for every component. 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: All components are represented by the subfolders found in the folders here:
`ldp` contains all LDP related components, `ldp` contains all LDP related components,
`identity` all IDP components, etc. `identity` all IDP components, etc.
Options are then chosen by importing 1 entry from every component subfolder. 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, More information on how this can be done manually,
it is always possible to not choose any of them and create your own custom version instead. 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 Below we give an overview of the main identifying features of the configurations.
that imports one option from every component subfolder We start with all features of the default configuration,
(such as either `allow-all.json` or `webacl.json` from `ldp/authorization`). after which we will explain in which features the other ones differ from it.
In case none of the available options suffice, there are 2 other ways to handle this:
### Append to an existing config ## default.json
In case the options mostly suffice, but they just need to do a bit more, This is the configuration that is used if no configuration is provided when starting the server.
it might be possible to append to one of the solutions. 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, For authorization, it uses [Web Access Control (WAC)](https://solid.github.io/web-access-control-spec/),
but an additional one needs to be added, it supports all [notification methods](https://solidproject.org/TR/notifications-protocol) implemented in CSS,
you could import `ldp/metadata-parser/default.json` allows users to create accounts, pods, WebIDs, and use them for [Solid-OIDC](https://solid.github.io/solid-oidc/).
and then add the following in your root config:
```json 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.
"@id": "urn:solid-server:default:MetadataParser",
"@type": "ParallelHandler",
"handlers": [
{ "@type": "MyNewParser" }
]
}
```
This will add the new parser to the list of metadata parsers. Although strictly not allowed by the Solid specification,
The `@id` value is needed so Components.js knows which object to add the values to, this configuration allows users to both write data at root level of the server,
and the `@type` is needed so it can interpret the other fields (`handlers` in this case). 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. ## file.json
In case the order is important this can not be guaranteed over separate files.
### 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, ## file-acp.json
the solution is to not import anything from that folder but instead write your own.
For example, in case you only want the slug parser but not any of the others, The only difference with `file.json`is that this uses
you would have to not import anything from `ldp/metadata-parser` folder, [Access Control Policy (ACP)](https://solid.github.io/authorization-panel/acp-specification/)
but instead have the following in your root config: for authorization instead of WAC.
```json ## file-root.json
{
"@id": "urn:solid-server:default:MetadataParser",
"@type": "ParallelHandler",
"handlers": [
{ "@type": "SlugParser" }
]
}
```
Don't forget that in some cases you would also have to copy some imports! This configuration starts from `file.json`, but does not allow the creation of accounts.
The existing options can be used as inspiration. 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.

View File

@ -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 * *all*: Supports all available notification types of the Solid Notifications protocol
[specification](https://solidproject.org/TR/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. * *disabled*: No notifications are sent out.
* *legacy-websocket*: Follows the legacy Solid WebSocket * *legacy-websocket*: Follows the legacy Solid WebSocket
[specification](https://github.com/solid/solid-spec/blob/master/api-websockets.md). [specification](https://github.com/solid/solid-spec/blob/master/api-websockets.md).
Will be removed in future versions. Will be removed in future versions.
* *new-old-websockets.json*: Support for both the legacy Solid Websockets and the new WebSocketChannel2023. * *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 * *webhooks*: Follows the WebhookChannel2023
[specification](https://solid.github.io/notifications/webhook-channel-2023) draft. [specification](https://solid.github.io/notifications/webhook-channel-2023) draft.
* *websockets*: Follows the WebSocketChannel2023 * *websockets*: Follows the WebSocketChannel2023

View File

@ -6,6 +6,7 @@
"css:config/http/notifications/base/http.json", "css:config/http/notifications/base/http.json",
"css:config/http/notifications/base/listener.json", "css:config/http/notifications/base/listener.json",
"css:config/http/notifications/base/storage.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/handler.json",
"css:config/http/notifications/websockets/http.json", "css:config/http/notifications/websockets/http.json",
"css:config/http/notifications/websockets/subscription.json", "css:config/http/notifications/websockets/subscription.json",

View 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."
}
]
}

View 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" }
]
}
]
}

View File

@ -39,9 +39,16 @@
}, },
"enabledJWA": { "enabledJWA": {
"dPoPSigningAlgValues": [ "dPoPSigningAlgValues": [
"RS256", "RS384", "RS512", "RS256",
"PS256", "PS384", "PS512", "RS384",
"ES256", "ES256K", "ES384", "ES512", "RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES256K",
"ES384",
"ES512",
"EdDSA" "EdDSA"
] ]
}, },

View File

@ -7,10 +7,15 @@
"@type": "SafeErrorHandler", "@type": "SafeErrorHandler",
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }, "showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
"errorHandler": { "errorHandler": {
"@id": "urn:solid-server:default:TargetExtractorErrorHandler",
"@type": "TargetExtractorErrorHandler",
"targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"errorHandler": {
"@id": "urn:solid-server:default:WaterfallErrorHandler",
"@type": "WaterfallHandler", "@type": "WaterfallHandler",
"handlers": [ "handlers": [
{ {
"comment": "Internally redirects are created by throwing a specific error, this handler converts them to the correct response.", "comment": "Redirects are created internally by throwing a specific error; this handler converts them to the correct response.",
"@type": "RedirectingErrorHandler" "@type": "RedirectingErrorHandler"
}, },
{ {
@ -23,5 +28,6 @@
] ]
} }
} }
}
] ]
} }

View File

@ -38,6 +38,7 @@ the [changelog](https://github.com/CommunitySolidServer/CommunitySolidServer/blo
* [How to automatically seed pods on startup](usage/seeding-pods.md) * [How to automatically seed pods on startup](usage/seeding-pods.md)
* [Receiving notifications when resources change](usage/notifications.md) * [Receiving notifications when resources change](usage/notifications.md)
* [Using the CSS as a development server in another project](usage/dev-configuration.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 ## What the internals look like

View File

@ -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, 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. 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 ```mermaid
flowchart TD flowchart TD
ServerInitializer("<strong>ServerInitializer</strong><br>ServerInitializer") ServerInitializer("<strong>ServerInitializer</strong><br>ServerInitializer")
ServerInitializer --> ServerInitializerArgs ServerInitializer --> ServerFactory("<strong>ServerFactory</strong><br>BaseServerFactory")
ServerFactory --> ServerConfigurator("<strong>ServerConfigurator</strong><br>ParallelHandler")
ServerConfigurator --> ServerConfiguratorArgs
subgraph ServerInitializerArgs[" "] subgraph ServerConfiguratorArgs[" "]
direction LR direction LR
ServerFactory("<strong>ServerFactory</strong><br>BaseServerFactory") HandlerServerConfigurator("<strong>HandlerServerConfigurator</strong><br>HandlerServerConfigurator")
ServerListener("<strong>ServerListener</strong><br>ParallelHandler") WebSocketServerConfigurator("<strong>WebSocketServerConfigurator</strong><br>WebSocketServerConfigurator")
end end
ServerListener --> HandlerServerListener("<strong>HandlerServerListener</strong><br>HandlerServerListener") HandlerServerConfigurator --> HttpHandler("<strong>HttpHandler</strong><br><i>HttpHandler</i>")
WebSocketServerConfigurator --> WebSocketHandler("<strong>WebSocketHandler</strong><br><i>WebSocketHandler</i>")
HandlerServerListener --> HttpHandler("<strong>HttpHandler</strong><br><i>HttpHandler</i>")
``` ```
The `HttpServerFactory` is responsible for starting a server on a given port. 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. Depending on the configuration this can be an HTTP or an HTTPS server.
The created server emits events when it receives requests. 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. Any requests it receives, it sends to its `ServerConfigurator`,
One listener that is always used is the `urn:solid-server:default:HandlerServerListener`, which handles the request as needed.
which calls an `HttpHandler` [to resolve HTTP requests](http-handler.md). This is a `ParallelHandler`, supporting two kinds of requests:
HTTP requests go through a configurator that sends those
Sometimes it is necessary to add additional listeners, to an `HttpHandler` [to resolve HTTP requests](http-handler.md).
these can then be added to the `urn:solid-server:default:ServerListener` as it is a `ParallellHandler`. In case WebSockets are enabled [to handle notifications](notifications.md),
An example of this is when WebSockets are used [to handle notifications](notifications.md). these are handled by the `WebSocketHandler`.

View File

@ -36,10 +36,10 @@ whose results get merged together in an `ArrayUnionHandler`.
A `NotificationChannelType` contains the specific details of a specification notification channel type, A `NotificationChannelType` contains the specific details of a specification notification channel type,
including a JSON-LD representation of the corresponding subscription resource. including a JSON-LD representation of the corresponding subscription resource.
One specific instance of a `StorageDescriber` is a `NotificationSubcriber`, One specific instance of a `StorageDescriber` is a `NotificationDescriber`,
which merges those JSON-LD descriptions into a single set of RDF quads. which merges those JSON-LD descriptions into a single set of RDF quads.
When adding a new subscription type, When adding a new subscription type,
a new instance of such a class should be added to the `urn:solid-server:default:StorageDescriber`. a new instance of such a class should be added to the `urn:solid-server:default:NotificationDescriber`.
## NotificationChannel ## NotificationChannel
@ -53,9 +53,9 @@ flowchart LR
subgraph NotificationTypeHandlerArgs[" "] subgraph NotificationTypeHandlerArgs[" "]
direction LR direction LR
OperationRouterHandler("<br>OperationRouterHandler") --> NotificationSubscriber("<br>NotificationSubscriber") OperationRouterHandler("<br>OperationRouterHandler") --> NotificationSubscriber("<br>NotificationSubscriber")
NotificationChannelType --> NotificationChannelType("<br><i>NotificationChannelType</i>") NotificationSubscriber --> NotificationChannelType("<br><i>NotificationChannelType</i>")
OperationRouterHandler2("<br>OperationRouterHandler") --> NotificationSubscriber2("<br>NotificationSubscriber") OperationRouterHandler2("<br>OperationRouterHandler") --> NotificationSubscriber2("<br>NotificationSubscriber")
NotificationChannelType2 --> NotificationChannelType2("<br><i>NotificationChannelType</i>") NotificationSubscriber2 --> NotificationChannelType2("<br><i>NotificationChannelType</i>")
end end
``` ```
@ -184,3 +184,45 @@ are quite similar to those needed for WebSocketChannel2023:
* The `WebhookChannel2023Type` class contains all the necessary typing information. * The `WebhookChannel2023Type` class contains all the necessary typing information.
* `WebhookEmitter` is the `NotificationEmitter` that sends the request. * `WebhookEmitter` is the `NotificationEmitter` that sends the request.
* `WebhookUnsubscriber` and `WebhookWebId` are additional utility classes to support the spec requirements. * `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
```

View 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.

View 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.

View File

@ -29,19 +29,42 @@ This only needs to be done once, afterwards this token can be used for all futur
Before doing the step below, Before doing the step below,
you already need to have an [authorization value](account/json-api.md#authorization) you already need to have an [authorization value](account/json-api.md#authorization)
that you get after logging in to your account. that you get after logging in to your account.
In the example below the cookie value is used.
In the default server configurations, Below is an example of how this would work with
you can log in through the [email/password API](account/json-api.md#controlspasswordlogin). the [email/password API](account/json-api.md#controlspasswordlogin)
from the default server configurations.
```ts ```ts
// This assumes your server is started under http://localhost:3000/. // All these examples assume the server is running at `http://localhost:3000/`.
// It also assumes you have already logged in and `cookie` contains a valid cookie header
// as described in the API documentation. // First we request the account API controls to find out where we can log in
const indexResponse = await fetch('http://localhost:3000/.account/', { headers: { cookie }}); const indexResponse = await fetch('http://localhost:3000/.account/');
const { controls } = await indexResponse.json(); const { controls } = await indexResponse.json();
// And then we log in to the account API
const response = await fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: 'my-email@example.com', password: 'my-password' }),
});
// This authorization value will be used to authenticate in the next step
const { authorization } = await response.json();
```
The next step generates the token and assumes you have an authorization value as generated in the example above.
```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/', {
headers: { authorization: `CSS-Account-Token ${authorization}` }
});
const { controls } = await indexResponse.json();
// Here we request the server to generate a token on our account
const response = await fetch(controls.account.clientCredentials, { const response = await fetch(controls.account.clientCredentials, {
method: 'POST', method: 'POST',
headers: { cookie, 'content-type': 'application/json' }, headers: { authorization: `CSS-Account-Token ${authorization}`, 'content-type': 'application/json' },
// The name field will be used when generating the ID of your token. // The name field will be used when generating the ID of your token.
// The WebID field determines which WebID you will identify as when using the token. // The WebID field determines which WebID you will identify as when using the token.
// Only WebIDs linked to your account can be used. // Only WebIDs linked to your account can be used.
@ -64,7 +87,6 @@ This Access Token is only valid for a certain amount of time, after which a new
```ts ```ts
import { createDpopHeader, generateDpopKeyPair } from '@inrupt/solid-client-authn-core'; import { createDpopHeader, generateDpopKeyPair } from '@inrupt/solid-client-authn-core';
import fetch from 'node-fetch';
// A key pair is needed for encryption. // A key pair is needed for encryption.
// This function from `solid-client-authn` generates such a pair for you. // This function from `solid-client-authn` generates such a pair for you.

View File

@ -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 ## Unsubscribing from a notification channel
!!! note !!! note

View File

@ -78,8 +78,10 @@ nav:
- Welcome: - Welcome:
- README.md - README.md
- Features: - Features:
- features.md - Overview: features.md
- Tests: features/test.md
- Usage: - Usage:
- Starting the server: usage/starting-server.md
- Example request: usage/example-requests.md - Example request: usage/example-requests.md
- Metadata: usage/metadata.md - Metadata: usage/metadata.md
- Identity provider: - Identity provider:
@ -91,6 +93,7 @@ nav:
- Seeding pods: usage/seeding-pods.md - Seeding pods: usage/seeding-pods.md
- Notifications: usage/notifications.md - Notifications: usage/notifications.md
- Development server: usage/dev-configuration.md - Development server: usage/dev-configuration.md
- Authorization methods: usage/authorization-methods.md
- Architecture: - Architecture:
- Overview: architecture/overview.md - Overview: architecture/overview.md
- Dependency injection: architecture/dependency-injection.md - Dependency injection: architecture/dependency-injection.md

View File

@ -1,45 +1,11 @@
const antfu = require('@antfu/eslint-config'); const opinionated = require('opinionated-eslint-config');
const generalConfig = require('./eslint/general');
const testConfig = require('./eslint/test');
const typedConfig = require('./eslint/typed');
const unicornConfig = require('./eslint/unicorn');
// The default ignore list contains all `output` folders, which conflicts with our src/http/output folder module.exports = opinionated(
// 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(
{ {
// Don't want to lint test assets, or TS snippets in markdown files // Don't want to lint test assets, or TS snippets in markdown files
ignores: [ 'test/assets/*', '**/*.md/**/*.ts' ], ignores: [ 'test/assets/*', '**/*.md' ],
}, typescript: {
generalConfig, tsconfigPath: [ './tsconfig.json', './scripts/tsconfig.json', './test/tsconfig.json' ],
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',
}, },
}, },
); );

View File

@ -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 },
],
},
};

View File

@ -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',
},
};

View File

@ -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,
},
}],
},
};
};

View File

@ -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',
},
};

View File

@ -70,7 +70,7 @@ module.exports = {
'^jose/(.*)$': '<rootDir>/node_modules/jose/dist/node/cjs/$1', '^jose/(.*)$': '<rootDir>/node_modules/jose/dist/node/cjs/$1',
}, },
// Slower machines had problems calling the WebSocket integration callbacks on time // Slower machines had problems calling the WebSocket integration callbacks on time
testTimeout: 60000, testTimeout: 90000,
reporters: ci ? [ 'default', 'github-actions' ] : [ 'default' ], reporters: ci ? [ 'default', 'github-actions' ] : [ 'default' ],
...ci && jestGithubRunnerSpecs(), ...ci && jestGithubRunnerSpecs(),

18345
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@solid/community-server", "name": "@solid/community-server",
"version": "7.0.3", "version": "7.1.2",
"description": "Community Solid Server: an open and modular implementation of the Solid specifications", "description": "Community Solid Server: an open and modular implementation of the Solid specifications",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/CommunitySolidServer/CommunitySolidServer#readme", "homepage": "https://github.com/CommunitySolidServer/CommunitySolidServer#readme",
@ -56,7 +56,6 @@
"lint": "npm run lint:eslint && npm run lint:markdown", "lint": "npm run lint:eslint && npm run lint:markdown",
"lint:eslint": "eslint . --cache --max-warnings 0", "lint:eslint": "eslint . --cache --max-warnings 0",
"lint:markdown": "markdownlint-cli2", "lint:markdown": "markdownlint-cli2",
"lint:markdown:fix": "markdownlint-cli2-fix",
"prepare": "npm run build", "prepare": "npm run build",
"release": "commit-and-tag-version", "release": "commit-and-tag-version",
"postrelease": "ts-node ./scripts/finalizeRelease.ts", "postrelease": "ts-node ./scripts/finalizeRelease.ts",
@ -121,7 +120,7 @@
"marked": "^14.1.0", "marked": "^14.1.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"n3": "^1.17.1", "n3": "^1.17.1",
"nodemailer": "^6.9.6", "nodemailer": "^6.9.9",
"oidc-provider": "^8.4.0", "oidc-provider": "^8.4.0",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"pump": "^3.0.0", "pump": "^3.0.0",
@ -143,32 +142,31 @@
"yup": "^1.3.2" "yup": "^1.3.2"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "2.3.4", "@commitlint/cli": "^19.3.0",
"@commitlint/cli": "^19.0.3", "@commitlint/config-conventional": "^19.2.2",
"@commitlint/config-conventional": "^19.0.3",
"@inrupt/solid-client-authn-core": "^2.0.0", "@inrupt/solid-client-authn-core": "^2.0.0",
"@inrupt/solid-client-authn-node": "^2.0.0", "@inrupt/solid-client-authn-node": "^2.0.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/jest": "^29.5.5", "@types/jest": "^29.5.12",
"@types/set-cookie-parser": "^2.4.4", "@types/set-cookie-parser": "^2.4.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"commit-and-tag-version": "^12.0.0", "commit-and-tag-version": "^12.0.0",
"componentsjs-generator": "^4.0.1", "componentsjs-generator": "^4.0.1",
"eslint-plugin-jest": "^27.4.3",
"husky": "^4.3.8", "husky": "^4.3.8",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-esm-transformer-2": "^1.0.0", "jest-esm-transformer-2": "^1.0.0",
"jest-rdf": "^1.8.0", "jest-rdf": "^1.8.0",
"markdownlint-cli2": "^0.10.0", "markdownlint-cli2": "^0.13.0",
"node-mocks-http": "^1.13.0", "node-mocks-http": "^1.13.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"opinionated-eslint-config": "0.1.0",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0",
"simple-git": "^3.20.0", "simple-git": "^3.20.0",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.2",
"typedoc": "^0.25.2", "typedoc": "^0.26.4",
"typescript": "^5.2.2" "typescript": "^5.5.3"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -179,7 +177,7 @@
"commit-and-tag-version": { "commit-and-tag-version": {
"scripts": { "scripts": {
"postbump": "ts-node ./scripts/upgradeConfig.ts", "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": { "writerOpts": {
"commitsSort": false "commitsSort": false

View File

@ -27,7 +27,9 @@ async function commitAndTag(): Promise<void> {
/** /**
* Prompts the user for input * Prompts the user for input
*
* @param query - A string to prompt the user * @param query - A string to prompt the user
*
* @returns Promise with the input of the user * @returns Promise with the input of the user
*/ */
async function waitForUserInput(query: string): Promise<string> { async function waitForUserInput(query: string): Promise<string> {

View File

@ -14,11 +14,13 @@ import { readFile, writeFile } from 'fs-extra';
/** /**
* Capitalize all list entries * Capitalize all list entries
*
* @param input - String to search/replace * @param input - String to search/replace
*
* @returns Promise with output string * @returns Promise with output string
*/ */
async function capitalizeListEntries(input: string): Promise<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 * Main function for changelog formatting
*
* @param filePath - Path to the changelog file * @param filePath - Path to the changelog file
*
* @returns Promise * @returns Promise
*/ */
async function formatChangelog(filePath: string): Promise<void> { async function formatChangelog(filePath: string): Promise<void> {

View File

@ -18,6 +18,7 @@ import { joinFilePath, readPackageJson } from '../src/util/PathUtil';
/** /**
* Search and replace the version of a component with given name * Search and replace the version of a component with given name
*
* @param filePath - File to search/replace * @param filePath - File to search/replace
* @param regex - RegExp matching the component reference * @param regex - RegExp matching the component reference
* @param version - Semantic version to change to * @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 * Recursive search for files that match a given Regex
*
* @param path - Path of folder to start search in * @param path - Path of folder to start search in
* @param regex - A regular expression to which file names will be matched * @param regex - A regular expression to which file names will be matched
*
* @returns Promise with all file pathss * @returns Promise with all file pathss
*/ */
async function getFilePaths(path: string, regex: RegExp): Promise<string[]> { async function getFilePaths(path: string, regex: RegExp): Promise<string[]> {

View File

@ -19,7 +19,7 @@ export class UnsecureWebIdExtractor extends CredentialsExtractor {
} }
public async handle({ headers }: HttpRequest): Promise<Credentials> { 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}`); this.logger.info(`Agent unsecurely claims to be ${webId}`);
return { agent: { webId }}; return { agent: { webId }};
} }

View File

@ -24,7 +24,7 @@ import { AclMode } from './permissions/AclPermissionSet';
import { AccessMode } from './permissions/Permissions'; import { AccessMode } from './permissions/Permissions';
import type { PermissionMap, PermissionSet } 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.Read]: [ AccessMode.read ],
[ACL.Write]: [ AccessMode.append, AccessMode.write ], [ACL.Write]: [ AccessMode.append, AccessMode.write ],
[ACL.Append]: [ AccessMode.append ], [ACL.Append]: [ AccessMode.append ],
@ -65,6 +65,7 @@ export class AcpReader extends PermissionReader {
/** /**
* Generates the allowed permissions. * Generates the allowed permissions.
*
* @param target - Target to generate permissions for. * @param target - Target to generate permissions for.
* @param credentials - Credentials that are trying to access the resource. * @param credentials - Credentials that are trying to access the resource.
* @param resourceCache - Cache used to store ACR data. * @param resourceCache - Cache used to store ACR data.

View File

@ -7,7 +7,7 @@ import type {
IPolicy, IPolicy,
} from '@solid/access-control-policy'; } from '@solid/access-control-policy';
import type { Store } from 'n3'; import type { Store } from 'n3';
import type { NamedNode, Term } from 'rdf-js'; import type { NamedNode, Term } from '@rdfjs/types';
import { ACP } from '../util/Vocabularies'; import { ACP } from '../util/Vocabularies';
/** /**
@ -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. * Finds the {@link IMatcher} with the given identifier in the given dataset.
*
* @param data - Dataset to look in. * @param data - Dataset to look in.
* @param matcher - Identifier of the matcher. * @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. * Finds the {@link IPolicy} with the given identifier in the given dataset.
*
* @param data - Dataset to look in. * @param data - Dataset to look in.
* @param policy - Identifier of the policy. * @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. * Finds the {@link IAccessControl} with the given identifier in the given dataset.
*
* @param data - Dataset to look in. * @param data - Dataset to look in.
* @param accessControl - Identifier of the access control. * @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. * Finds the {@link IAccessControlResource} with the given identifier in the given dataset.
*
* @param data - Dataset to look in. * @param data - Dataset to look in.
* @param acr - Identifier of the access control resource. * @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. * Finds all {@link IAccessControlledResource} in the given dataset.
*
* @param data - Dataset to look in. * @param data - Dataset to look in.
*/ */
export function* getAccessControlledResources(data: Store): Iterable<IAccessControlledResource> { export function* getAccessControlledResources(data: Store): Iterable<IAccessControlledResource> {

View File

@ -102,7 +102,9 @@ export class ParentContainerReader extends PermissionReader {
// When an operation requests to delete a resource, // When an operation requests to delete a resource,
// the server MUST match Authorizations allowing the acl:Write access privilege // the server MUST match Authorizations allowing the acl:Write access privilege
// on the resource and the containing container. // on the resource and the containing container.
mergedPermission.delete = resourcePermission.write && containerPermission.write && mergedPermission.delete =
resourcePermission.write &&
containerPermission.write &&
resourcePermission.delete !== false; resourcePermission.delete !== false;
return mergedPermission; return mergedPermission;

View File

@ -24,6 +24,7 @@ export class PermissionBasedAuthorizer extends Authorizer {
/** /**
* The existence of the target resource determines the output status code for certain situations. * The existence of the target resource determines the output status code for certain situations.
* The provided {@link ResourceSet} will be used for that. * The provided {@link ResourceSet} will be used for that.
*
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence. * @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
*/ */
public constructor(resourceSet: ResourceSet) { 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. * 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 * Throws a {@link ForbiddenHttpError} or {@link UnauthorizedHttpError} depending on the credentials
* if access is not allowed. * if access is not allowed.
*
* @param credentials - Credentials that require access. * @param credentials - Credentials that require access.
* @param permissionSet - PermissionSet describing the available permissions of the credentials. * @param permissionSet - PermissionSet describing the available permissions of the credentials.
* @param mode - Which mode is requested. * @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). * Checks whether the agent is authenticated (logged in) or not (public/anonymous).
*
* @param credentials - Credentials to check. * @param credentials - Credentials to check.
*/ */
private isAuthenticated(credentials: Credentials): boolean { private isAuthenticated(credentials: Credentials): boolean {

View File

@ -22,7 +22,7 @@ import type { PermissionMap } from './permissions/Permissions';
import { AccessMode } from './permissions/Permissions'; import { AccessMode } from './permissions/Permissions';
// Maps WebACL-specific modes to generic access modes. // 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.Read]: [ AccessMode.read ],
[ACL.Write]: [ AccessMode.append, AccessMode.write ], [ACL.Write]: [ AccessMode.append, AccessMode.write ],
[ACL.Append]: [ AccessMode.append ], [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'}`); this.logger.debug(`Retrieving permissions of ${credentials.agent?.webId ?? 'an unknown agent'}`);
const aclMap = await this.getAclMatches(requestedModes.distinctKeys()); const aclMap = await this.getAclMatches(requestedModes.distinctKeys());
const storeMap = await this.findAuthorizationStatements(aclMap); 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. * Determines the available permissions for the given credentials.
*
* @param acl - Store containing all relevant authorization triples. * @param acl - Store containing all relevant authorization triples.
* @param credentials - Credentials to find the permissions for. * @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, * Extracts all rules from the store that are relevant for the given target,
* based on either the `acl:accessTo` or `acl:default` predicates. * based on either the `acl:accessTo` or `acl:default` predicates.
*
* @param store - Store to filter. * @param store - Store to filter.
* @param target - The identifier of which the acl rules need to be known. * @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. * @param directAcl - If the store contains triples from the direct acl resource of the target or not.

View File

@ -21,7 +21,7 @@ export class AgentGroupAccessChecker extends AccessChecker {
const { webId } = credentials.agent; const { webId } = credentials.agent;
const groups = acl.getObjects(rule, ACL.terms.agentGroup, null); 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))); this.isMemberOfGroup(webId, group)));
} }
return false; return false;
@ -29,6 +29,7 @@ export class AgentGroupAccessChecker extends AccessChecker {
/** /**
* Checks if the given agent is member of a given vCard group. * Checks if the given agent is member of a given vCard group.
*
* @param webId - WebID of the agent that needs access. * @param webId - WebID of the agent that needs access.
* @param group - URL of the vCard group that needs to be checked. * @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); const representation = await fetchDataset(url);
return readableToQuads(representation.data); return readableToQuads(representation.data);
})(); })();
return await prom; return prom;
} }
} }

View File

@ -19,6 +19,7 @@ export class IntermediateCreateExtractor extends ModesExtractor {
/** /**
* Certain permissions depend on the existence of the target resource. * Certain permissions depend on the existence of the target resource.
* The provided {@link ResourceSet} will be used for that. * The provided {@link ResourceSet} will be used for that.
*
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence. * @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
* @param strategy - {@link IdentifierStrategy} that will be used to determine parent containers. * @param strategy - {@link IdentifierStrategy} that will be used to determine parent containers.
* @param source - The source {@link ModesExtractor}. * @param source - The source {@link ModesExtractor}.

View File

@ -20,6 +20,7 @@ export class MethodModesExtractor extends ModesExtractor {
/** /**
* Certain permissions depend on the existence of the target resource. * Certain permissions depend on the existence of the target resource.
* The provided {@link ResourceSet} will be used for that. * The provided {@link ResourceSet} will be used for that.
*
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence. * @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
*/ */
public constructor(resourceSet: ResourceSet) { public constructor(resourceSet: ResourceSet) {

View File

@ -22,6 +22,7 @@ export class N3PatchModesExtractor extends ModesExtractor {
/** /**
* Certain permissions depend on the existence of the target resource. * Certain permissions depend on the existence of the target resource.
* The provided {@link ResourceSet} will be used for that. * The provided {@link ResourceSet} will be used for that.
*
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence. * @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
*/ */
public constructor(resourceSet: ResourceSet) { public constructor(resourceSet: ResourceSet) {

View File

@ -20,6 +20,7 @@ export class SparqlUpdateModesExtractor extends ModesExtractor {
/** /**
* Certain permissions depend on the existence of the target resource. * Certain permissions depend on the existence of the target resource.
* The provided {@link ResourceSet} will be used for that. * The provided {@link ResourceSet} will be used for that.
*
* @param resourceSet - {@link ResourceSet} that can verify the target resource existence. * @param resourceSet - {@link ResourceSet} that can verify the target resource existence.
*/ */
public constructor(resourceSet: ResourceSet) { public constructor(resourceSet: ResourceSet) {

View File

@ -79,7 +79,7 @@ class WebSocketListener extends WebSocketListenerEmitter {
private onMessage(message: string): void { private onMessage(message: string): void {
// Parse the message // Parse the message
const match = /^(\w+)\s+(.+)$/u.exec(message); const match = /^(\w+)\s+(\S.+)$/u.exec(message);
if (!match) { if (!match) {
this.sendMessage('warning', `Unrecognized message format: ${message}`); this.sendMessage('warning', `Unrecognized message format: ${message}`);
return; return;

View File

@ -31,6 +31,7 @@ export interface AuxiliaryIdentifierStrategy {
* Checks if the input identifier corresponds to an auxiliary resource. * Checks if the input identifier corresponds to an auxiliary resource.
* This does not check if that auxiliary resource exists, * This does not check if that auxiliary resource exists,
* only if the identifier indicates that there could be an auxiliary resource there. * only if the identifier indicates that there could be an auxiliary resource there.
*
* @param identifier - Identifier to check. * @param identifier - Identifier to check.
* *
* @returns true if the input identifier points to an auxiliary resource. * @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. * Returns the identifier of the resource which this auxiliary resource is referring to.
* This does not guarantee that this resource exists. * This does not guarantee that this resource exists.
*
* @param identifier - Identifier of the auxiliary resource. * @param identifier - Identifier of the auxiliary resource.
* *
* @returns The ResourceIdentifier of the subject resource. * @returns The ResourceIdentifier of the subject resource.

View File

@ -11,6 +11,7 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy { export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/** /**
* Whether this auxiliary resources uses its own authorization instead of the subject resource authorization. * Whether this auxiliary resources uses its own authorization instead of the subject resource authorization.
*
* @param identifier - Identifier of the auxiliary resource. * @param identifier - Identifier of the auxiliary resource.
*/ */
usesOwnAuthorization: (identifier: ResourceIdentifier) => boolean; 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. * 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. * If yes, this means they can't be deleted individually from such a container.
*
* @param identifier - Identifier of the auxiliary resource. * @param identifier - Identifier of the auxiliary resource.
*/ */
isRequiredInRoot: (identifier: ResourceIdentifier) => boolean; isRequiredInRoot: (identifier: ResourceIdentifier) => boolean;
@ -42,6 +44,7 @@ export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/** /**
* Validates if the representation contains valid data for an auxiliary resource. * Validates if the representation contains valid data for an auxiliary resource.
* Should throw an error in case the data is invalid. * Should throw an error in case the data is invalid.
*
* @param identifier - Identifier of the auxiliary resource. * @param identifier - Identifier of the auxiliary resource.
* @param representation - Representation of the auxiliary resource. * @param representation - Representation of the auxiliary resource.
*/ */

View File

@ -1,5 +1,5 @@
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js'; import type { NamedNode } from '@rdfjs/types';
import { SOLID_META } from '../../util/Vocabularies'; import { SOLID_META } from '../../util/Vocabularies';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'; import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';

View File

@ -1,5 +1,5 @@
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js'; import type { NamedNode } from '@rdfjs/types';
import type { HttpRequest } from '../../../server/HttpRequest'; import type { HttpRequest } from '../../../server/HttpRequest';
import { matchesAuthorizationScheme } from '../../../util/HeaderUtil'; import { matchesAuthorizationScheme } from '../../../util/HeaderUtil';
import { SOLID_META } from '../../../util/Vocabularies'; import { SOLID_META } from '../../../util/Vocabularies';

View File

@ -1,6 +1,6 @@
import { parse } from 'cookie'; import { parse } from 'cookie';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js'; import type { NamedNode } from '@rdfjs/types';
import type { HttpRequest } from '../../../server/HttpRequest'; import type { HttpRequest } from '../../../server/HttpRequest';
import { SOLID_META } from '../../../util/Vocabularies'; import { SOLID_META } from '../../../util/Vocabularies';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';

View File

@ -50,7 +50,9 @@ export class LinkRelObject {
/** /**
* Checks whether the object can be added to the metadata * Checks whether the object can be added to the metadata
*
* @param object - The link target. * @param object - The link target.
*
* @returns a boolean to indicate whether it can be added to the metadata or not * @returns a boolean to indicate whether it can be added to the metadata or not
*/ */
private objectAllowed(object: string): boolean { private objectAllowed(object: string): boolean {
@ -59,6 +61,7 @@ export class LinkRelObject {
/** /**
* Adds the object to the metadata when it is allowed * Adds the object to the metadata when it is allowed
*
* @param object - The link target. * @param object - The link target.
* @param metadata - Metadata of the resource. * @param metadata - Metadata of the resource.
* @param logger - Logger * @param logger - Logger
@ -67,8 +70,10 @@ export class LinkRelObject {
if (this.objectAllowed(object)) { if (this.objectAllowed(object)) {
if (this.ephemeral) { if (this.ephemeral) {
metadata.add(this.value, namedNode(object), SOLID_META.terms.ResponseMetadata); metadata.add(this.value, namedNode(object), SOLID_META.terms.ResponseMetadata);
logger.debug(`"<${metadata.identifier.value}> <${this.value.value}> <${object}>." ` + logger.debug(
`will not be stored permanently in the metadata.`); `"<${metadata.identifier.value}> <${this.value.value}> <${object}>." ` +
`will not be stored permanently in the metadata.`,
);
} else { } else {
metadata.add(this.value, namedNode(object)); metadata.add(this.value, namedNode(object));
} }

View File

@ -1,4 +1,4 @@
import type { Term } from 'rdf-js'; import type { Term } from '@rdfjs/types';
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import type { ResourceStore } from '../../storage/ResourceStore'; import type { ResourceStore } from '../../storage/ResourceStore';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';

View File

@ -62,8 +62,8 @@ export class ConvertingErrorHandler extends ErrorHandler {
private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> { private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> {
if (!this.showStackTrace) { if (!this.showStackTrace) {
delete error.stack; delete error.stack;
// eslint-disable-next-line ts/no-unsafe-member-access // Cheating here to delete a readonly field
delete (error as any).cause; delete (error as { cause: unknown }).cause;
} }
const representation = new BasicRepresentation([ error ], error.metadata, INTERNAL_ERROR, false); const representation = new BasicRepresentation([ error ], error.metadata, INTERNAL_ERROR, false);
const identifier = { path: representation.metadata.identifier.value }; const identifier = { path: representation.metadata.identifier.value };

View 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);
}
}

View File

@ -8,8 +8,11 @@ import { LDP, PIM, RDF, SOLID_ERROR } from '../../../util/Vocabularies';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter'; import { MetadataWriter } from './MetadataWriter';
// Only PUT and PATCH can be used to create a new resource enum ResourceType {
const NEW_RESOURCE_ALLOWED_METHODS = new Set([ 'PUT', 'PATCH' ]); document,
container,
unknown,
}
/** /**
* Generates Allow, Accept-Patch, Accept-Post, and Accept-Put headers. * 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> { public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
const { response, metadata } = input; 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 // Filter out methods which are not allowed
const allowedMethods = this.filterAllowedMethods(metadata); const allowedMethods = this.filterAllowedMethods(metadata, resourceType);
// Generate the Allow headers (if required) // Generate the Allow headers (if required)
const generateAllow = this.generateAllow(allowedMethods, response, metadata); 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. * 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) const disallowedMethods = new Set(metadata.getAll(SOLID_ERROR.terms.disallowedMethod)
.map((term): string => term.value)); .map((term): string => term.value));
const allowedMethods = new Set(this.supportedMethods.filter((method): boolean => !disallowedMethods.has(method))); const allowedMethods = new Set(this.supportedMethods.filter((method): boolean => !disallowedMethods.has(method)));
// POST is only allowed on containers. // POST is only allowed on containers.
// Metadata only has the resource URI in case it has resource metadata. // Metadata only has the resource URI in case it has resource metadata.
if (!this.isPostAllowed(metadata)) { if (!this.isPostAllowed(resourceType)) {
allowedMethods.delete('POST'); allowedMethods.delete('POST');
} }
if (!this.isPutAllowed(metadata)) { if (!this.isPutAllowed(metadata, resourceType)) {
allowedMethods.delete('PUT'); allowedMethods.delete('PUT');
} }
if (!this.isDeleteAllowed(metadata)) { if (!this.isPatchAllowed(resourceType)) {
allowedMethods.delete('PATCH');
}
if (!this.isDeleteAllowed(metadata, resourceType)) {
allowedMethods.delete('DELETE'); allowedMethods.delete('DELETE');
} }
// If we are sure the resource does not exist: only keep methods that can create a new resource. // 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)) { if (metadata.has(SOLID_ERROR.terms.errorResponse, NotFoundHttpError.uri)) {
for (const method of allowedMethods) { 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); allowedMethods.delete(method);
} }
} }
@ -76,18 +96,23 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
/** /**
* POST is only allowed on containers. * 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 { private isPostAllowed(resourceType: ResourceType): boolean {
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || isContainerPath(metadata.identifier.value); return resourceType !== ResourceType.document;
} }
/** /**
* PUT is not allowed on existing containers. * PUT is not allowed on existing containers.
*/ */
private isPutAllowed(metadata: RepresentationMetadata): boolean { private isPutAllowed(metadata: RepresentationMetadata, resourceType: ResourceType): boolean {
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || !isContainerPath(metadata.identifier.value); 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. * Note that the identifier value check only works if the metadata is not about an error.
*/ */
private isDeleteAllowed(metadata: RepresentationMetadata): boolean { private isDeleteAllowed(metadata: RepresentationMetadata, resourceType: ResourceType): boolean {
if (!isContainerPath(metadata.identifier.value)) { if (resourceType !== ResourceType.container) {
return true; return true;
} }
const isStorage = metadata.has(RDF.terms.type, PIM.terms.Storage); const isStorage = metadata.has(RDF.terms.type, PIM.terms.Storage);
const isEmpty = metadata.has(LDP.terms.contains); const isEmpty = !metadata.has(LDP.terms.contains);
return !isStorage && !isEmpty; return !isStorage && isEmpty;
} }
/** /**

View File

@ -1,9 +1,10 @@
import { Util } from 'n3';
import { getLoggerFor } from '../../../logging/LogUtil'; import { getLoggerFor } from '../../../logging/LogUtil';
import type { HttpResponse } from '../../../server/HttpResponse'; import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/HeaderUtil'; import { addHeader } from '../../../util/HeaderUtil';
import { LDP, RDF, SOLID_ERROR } from '../../../util/Vocabularies';
import type { AuxiliaryStrategy } from '../../auxiliary/AuxiliaryStrategy'; import type { AuxiliaryStrategy } from '../../auxiliary/AuxiliaryStrategy';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../representation/ResourceIdentifier';
import { MetadataWriter } from './MetadataWriter'; import { MetadataWriter } from './MetadataWriter';
/** /**
@ -30,9 +31,17 @@ export class AuxiliaryLinkMetadataWriter extends MetadataWriter {
} }
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> { 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. // 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); const auxiliaryIdentifier = this.specificStrategy.getAuxiliaryIdentifier(identifier);
addHeader(input.response, 'Link', `<${auxiliaryIdentifier.path}>; rel="${this.relationType}"`); addHeader(input.response, 'Link', `<${auxiliaryIdentifier.path}>; rel="${this.relationType}"`);
} }

View File

@ -1,4 +1,4 @@
import type { Term } from 'rdf-js'; import type { Term } from '@rdfjs/types';
import type { HttpResponse } from '../../../server/HttpResponse'; import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/HeaderUtil'; import { addHeader } from '../../../util/HeaderUtil';
import { ACL, AUTH } from '../../../util/Vocabularies'; import { ACL, AUTH } from '../../../util/Vocabularies';

View File

@ -35,7 +35,7 @@ export class BasicRepresentation implements Representation {
* @param binary - Whether the representation is a binary or object stream * @param binary - Whether the representation is a binary or object stream
*/ */
public constructor( public constructor(
data: Guarded<Readable> | Readable | any[] | string, data: Guarded<Readable> | Readable | unknown[] | string,
metadata: RepresentationMetadata | MetadataRecord, metadata: RepresentationMetadata | MetadataRecord,
binary?: boolean, binary?: boolean,
); );
@ -47,7 +47,7 @@ export class BasicRepresentation implements Representation {
* @param binary - Whether the representation is a binary or object stream * @param binary - Whether the representation is a binary or object stream
*/ */
public constructor( public constructor(
data: Guarded<Readable> | Readable | any[] | string, data: Guarded<Readable> | Readable | unknown[] | string,
metadata: RepresentationMetadata | MetadataRecord, metadata: RepresentationMetadata | MetadataRecord,
contentType?: string, contentType?: string,
binary?: boolean, binary?: boolean,
@ -59,7 +59,7 @@ export class BasicRepresentation implements Representation {
* @param binary - Whether the representation is a binary or object stream * @param binary - Whether the representation is a binary or object stream
*/ */
public constructor( public constructor(
data: Guarded<Readable> | Readable | any[] | string, data: Guarded<Readable> | Readable | unknown[] | string,
contentType: string, contentType: string,
binary?: boolean, binary?: boolean,
); );
@ -71,7 +71,7 @@ export class BasicRepresentation implements Representation {
* @param binary - Whether the representation is a binary or object stream * @param binary - Whether the representation is a binary or object stream
*/ */
public constructor( public constructor(
data: Guarded<Readable> | Readable | any[] | string, data: Guarded<Readable> | Readable | unknown[] | string,
identifier: MetadataIdentifier, identifier: MetadataIdentifier,
metadata?: MetadataRecord, metadata?: MetadataRecord,
binary?: boolean, binary?: boolean,
@ -84,14 +84,14 @@ export class BasicRepresentation implements Representation {
* @param binary - Whether the representation is a binary or object stream * @param binary - Whether the representation is a binary or object stream
*/ */
public constructor( public constructor(
data: Guarded<Readable> | Readable | any[] | string, data: Guarded<Readable> | Readable | unknown[] | string,
identifier: MetadataIdentifier, identifier: MetadataIdentifier,
contentType?: string, contentType?: string,
binary?: boolean, binary?: boolean,
); );
public constructor( public constructor(
data?: Readable | any[] | string, data?: Readable | unknown[] | string,
metadata?: RepresentationMetadata | MetadataRecord | MetadataIdentifier | string, metadata?: RepresentationMetadata | MetadataRecord | MetadataIdentifier | string,
metadataRest?: MetadataRecord | string | boolean, metadataRest?: MetadataRecord | string | boolean,
binary?: boolean, binary?: boolean,
@ -109,8 +109,7 @@ export class BasicRepresentation implements Representation {
} }
if (!isRepresentationMetadata(metadata) || typeof metadataRest === 'string') { if (!isRepresentationMetadata(metadata) || typeof metadataRest === 'string') {
// This combination will always match with a valid overload // This combination will always match with a valid overload
// eslint-disable-next-line ts/no-unsafe-argument metadata = new RepresentationMetadata(metadata as RepresentationMetadata, metadataRest as string);
metadata = new RepresentationMetadata(metadata as any, metadataRest as any);
} }
this.metadata = metadata; this.metadata = metadata;

View File

@ -1,4 +1,4 @@
import type { Quad } from 'rdf-js'; import type { Quad } from '@rdfjs/types';
import type { Patch } from './Patch'; import type { Patch } from './Patch';
/** /**

View File

@ -1,5 +1,5 @@
import { DataFactory, Store } from 'n3'; import { DataFactory, Store } from 'n3';
import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from 'rdf-js'; import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from '@rdfjs/types';
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { ContentType, SIMPLE_MEDIA_RANGE } from '../../util/Header'; import { ContentType, SIMPLE_MEDIA_RANGE } from '../../util/Header';
import { isTerm, toLiteral, toNamedTerm, toObjectTerm } from '../../util/TermUtil'; import { isTerm, toLiteral, toNamedTerm, toObjectTerm } from '../../util/TermUtil';
@ -15,7 +15,7 @@ export type MetadataGraph = NamedNode | BlankNode | DefaultGraph | string;
/** /**
* Determines whether the object is a `RepresentationMetadata`. * 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'; 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. * Converts the incoming name (URI or shorthand) to a named node.
* The generated terms get cached to reduce the number of created nodes, * The generated terms get cached to reduce the number of created nodes,
* so only use this for internal constants! * so only use this for internal constants!
*
* @param name - Predicate to potentially transform. * @param name - Predicate to potentially transform.
*/ */
function toCachedNamedNode(name: string): NamedNode { function toCachedNamedNode(name: string): NamedNode {
@ -167,6 +168,7 @@ export class RepresentationMetadata {
/** /**
* Helper function to import all entries from the given metadata. * Helper function to import all entries from the given metadata.
* If the new metadata has a different identifier the internal one will be updated. * If the new metadata has a different identifier the internal one will be updated.
*
* @param metadata - Metadata to import. * @param metadata - Metadata to import.
*/ */
public setMetadata(metadata: RepresentationMetadata): this { public setMetadata(metadata: RepresentationMetadata): this {
@ -235,22 +237,24 @@ export class RepresentationMetadata {
/** /**
* Adds a value linked to the identifier. Strings get converted to literals. * Adds a value linked to the identifier. Strings get converted to literals.
*
* @param predicate - Predicate linking identifier to value. * @param predicate - Predicate linking identifier to value.
* @param object - Value(s) to add. * @param object - Value(s) to add.
* @param graph - Optional graph of where to add the values to. * @param graph - Optional graph of where to add the values to.
*/ */
public add(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this { 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. * Removes the given value from the metadata. Strings get converted to literals.
*
* @param predicate - Predicate linking identifier to value. * @param predicate - Predicate linking identifier to value.
* @param object - Value(s) to remove. * @param object - Value(s) to remove.
* @param graph - Optional graph of where to remove the values from. * @param graph - Optional graph of where to remove the values from.
*/ */
public remove(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this { 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. * Removes all values linked through the given predicate.
*
* @param predicate - Predicate to remove. * @param predicate - Predicate to remove.
* @param graph - Optional graph where to remove from. * @param graph - Optional graph where to remove from.
*/ */
@ -290,12 +295,17 @@ export class RepresentationMetadata {
): boolean { ): boolean {
// This works with N3.js but at the time of writing the typings have not been updated yet. // 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. // 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 as unknown as {
return (this.store.has as any)(this.id, predicate, object, graph) as boolean; 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. * Finds all object values matching the given predicate and/or graph.
*
* @param predicate - Optional predicate to get the values for. * @param predicate - Optional predicate to get the values for.
* @param graph - Optional graph where to get from. * @param graph - Optional graph where to get from.
* *
@ -310,10 +320,10 @@ export class RepresentationMetadata {
* @param predicate - Predicate to get the value for. * @param predicate - Predicate to get the value for.
* @param graph - Optional graph where the triple should be found. * @param graph - Optional graph where the triple should be found.
* *
* @returns The corresponding value. Undefined if there is no match
*
* @throws Error * @throws Error
* If there are multiple matching values. * If there are multiple matching values.
*
* @returns The corresponding value. Undefined if there is no match
*/ */
public get(predicate: NamedNode, graph?: MetadataGraph): Term | undefined { public get(predicate: NamedNode, graph?: MetadataGraph): Term | undefined {
const terms = this.getAll(predicate, graph); const terms = this.getAll(predicate, graph);
@ -333,6 +343,7 @@ export class RepresentationMetadata {
/** /**
* Sets the value for the given predicate, removing all other instances. * Sets the value for the given predicate, removing all other instances.
* In case the object is undefined this is identical to `removeAll(predicate)`. * In case the object is undefined this is identical to `removeAll(predicate)`.
*
* @param predicate - Predicate linking to the value. * @param predicate - Predicate linking to the value.
* @param object - Value(s) to set. * @param object - Value(s) to set.
* @param graph - Optional graph where the triple should be stored. * @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. * 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. * @returns A {@link ContentType} object containing the value and optional parameters if there is one.
*/ */
private getContentType(): ContentType | undefined { private getContentType(): ContentType | undefined {

View File

@ -12,5 +12,5 @@ export interface ResourceIdentifier {
* Determines whether the object is a {@link ResourceIdentifier}. * Determines whether the object is a {@link ResourceIdentifier}.
*/ */
export function isResourceIdentifier(object: unknown): object is 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';
} }

View File

@ -187,14 +187,20 @@ export class IdentityProviderFactory implements ProviderFactory {
provider.use(async(ctx, next): Promise<void> => { provider.use(async(ctx, next): Promise<void> => {
const accepts = ctx.accepts.bind(ctx); const accepts = ctx.accepts.bind(ctx);
// Using `any` typings to make sure we support all different versions of `ctx.accepts` // This is how you get the correct typing for an overloaded function
ctx.accepts = (...types): any => { 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 // Make sure we only override our specific case
if (types.length === 2 && types[0] === 'json' && types[1] === 'html') { if (types.length === 2 && types[0] === 'json' && types[1] === 'html') {
return 'html'; return 'html';
} }
return accepts(...types as string[]); return accepts(...types as string[]);
}; }) as AcceptFn;
return next(); 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. * 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'; 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 // Some fields are still missing, see https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1154#issuecomment-1040233385
config.findAccount = async(ctx: KoaContextWithOIDC, sub: string): Promise<Account> => ({ config.findAccount = async(ctx: KoaContextWithOIDC, sub: string): Promise<Account> => ({
accountId: sub, 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 }; return { sub, webid: sub, azp: ctx.oidc.client?.clientId };
}, },
}); });

View File

@ -1,5 +1,6 @@
import type { Json } from '../../util/Json';
import { ACCOUNT_ID_KEY } from './account/AccountIdRoute'; import { ACCOUNT_ID_KEY } from './account/AccountIdRoute';
import type { Json, JsonRepresentation } from './InteractionUtil'; import type { JsonRepresentation } from './InteractionUtil';
import type { JsonInteractionHandlerInput } from './JsonInteractionHandler'; import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
import { JsonInteractionHandler } from './JsonInteractionHandler'; import { JsonInteractionHandler } from './JsonInteractionHandler';
import type { InteractionRoute } from './routing/InteractionRoute'; import type { InteractionRoute } from './routing/InteractionRoute';

View File

@ -3,16 +3,12 @@ import type Provider from '../../../templates/types/oidc-provider';
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import type { Json } from '../../util/Json';
import type { Interaction } from './InteractionHandler'; import type { Interaction } from './InteractionHandler';
import Dict = NodeJS.Dict; import Dict = NodeJS.Dict;
const logger = getLoggerFor('AccountUtil'); const logger = getLoggerFor('AccountUtil');
/**
* A JSON object.
*/
export type Json = string | number | boolean | Dict<Json> | Json[];
/** /**
* Contains a JSON object and any associated metadata. * 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 * 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. * Updates the `oidcInteraction` object with the necessary data in case a prompt gets updated.
*
* @param oidcInteraction - Interaction to update. * @param oidcInteraction - Interaction to update.
* @param result - New data to add to the interaction. * @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. * @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, * Removes the WebID, the `accountId`, from the OIDC session object,
* allowing us to replace it with a new value. * allowing us to replace it with a new value.
* If there is no session in the Interaction, nothing will happen. * If there is no session in the Interaction, nothing will happen.
*
* @param provider - The OIDC provider. * @param provider - The OIDC provider.
* @param oidcInteraction - The current interaction. * @param oidcInteraction - The current interaction.
*/ */
@ -86,4 +84,12 @@ export async function forgetWebId(provider: Provider, oidcInteraction: Interacti
delete session.accountId; delete session.accountId;
await session.persist(); await session.persist();
} }
// If a client previously successfully completed an interaction, a grant will have been created.
// If such a session is reused to authenticate with a different WebID, we need to
// first delete the previously created grant, as the oidc-provider will try to reuse it as well.
if (oidcInteraction.grantId) {
const grant = await provider.Grant.find(oidcInteraction.grantId);
await grant?.destroy();
}
} }

View File

@ -3,10 +3,10 @@ import type { Representation } from '../../http/representation/Representation';
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter'; import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
import { APPLICATION_JSON } from '../../util/ContentTypes'; import { APPLICATION_JSON } from '../../util/ContentTypes';
import type { Json } from '../../util/Json';
import { readJsonStream } from '../../util/StreamUtil'; import { readJsonStream } from '../../util/StreamUtil';
import type { InteractionHandlerInput } from './InteractionHandler'; import type { InteractionHandlerInput } from './InteractionHandler';
import { InteractionHandler } from './InteractionHandler'; import { InteractionHandler } from './InteractionHandler';
import type { Json } from './InteractionUtil';
import type { JsonInteractionHandler, JsonInteractionHandlerInput } from './JsonInteractionHandler'; import type { JsonInteractionHandler, JsonInteractionHandlerInput } from './JsonInteractionHandler';
/** /**

View File

@ -1,8 +1,9 @@
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { AsyncHandler } from '../../util/handlers/AsyncHandler'; import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import type { Json } from '../../util/Json';
import type { Interaction } from './InteractionHandler'; import type { Interaction } from './InteractionHandler';
import type { Json, JsonRepresentation } from './InteractionUtil'; import type { JsonRepresentation } from './InteractionUtil';
import Dict = NodeJS.Dict; import Dict = NodeJS.Dict;
export interface JsonInteractionHandlerInput { export interface JsonInteractionHandlerInput {

View File

@ -1,5 +1,5 @@
import type { Json } from '../../util/Json';
import { ControlHandler } from './ControlHandler'; import { ControlHandler } from './ControlHandler';
import type { Json } from './InteractionUtil';
import type { JsonInteractionHandlerInput } from './JsonInteractionHandler'; import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
/** /**

View File

@ -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'; import { JsonInteractionHandler } from './JsonInteractionHandler';
/** /**

View File

@ -1,14 +1,16 @@
import { string } from 'yup'; 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 { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { createErrorMessage } from '../../util/errors/ErrorUtil'; import { createErrorMessage } from '../../util/errors/ErrorUtil';
import type { Json } from '../../util/Json';
import { isUrl } from '../../util/StringUtil'; import { isUrl } from '../../util/StringUtil';
import type { Json } from './InteractionUtil';
import Dict = NodeJS.Dict; 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 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, // We validate the WebID URL to prevent generation of invalid ACL,
// which would break the pod creation causing us to have an incomplete pod. // which would break the pod creation, causing us to have an incomplete pod.
export const URL_SCHEMA = string().trim().optional().test({ export const URL_SCHEMA = string().trim().optional().test({
name: 'url', name: 'url',
message: (value: { value: string }): string => `"${value.value}" is not a valid 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'; return schema.type === 'object';
} }
// `T` can't extend Schema since it could also be a Reference, which is a type `yup` doesn't export // `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 // 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 // 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]> }}; { 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. * 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); const result = parseSchemaDescription(schema);
return { fields: result.fields }; 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. * 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, schema: T,
data: unknown, data: unknown,
options?: ValidateOptions<any>, options?: ValidateOptions<AnyObject>,
): Promise<T['__outputType']> { ): Promise<T['__outputType']> {
try { try {
// eslint-disable-next-line ts/no-unsafe-return
return await schema.validate(data, options); return await schema.validate(data, options);
} catch (error: unknown) { } catch (error: unknown) {
throw new BadRequestHttpError(createErrorMessage(error)); throw new BadRequestHttpError(createErrorMessage(error));

View File

@ -5,7 +5,6 @@ export const ACCOUNT_SETTINGS_REMEMBER_LOGIN = 'rememberLogin';
export type AccountSettings = { [ACCOUNT_SETTINGS_REMEMBER_LOGIN]?: boolean }; export type AccountSettings = { [ACCOUNT_SETTINGS_REMEMBER_LOGIN]?: boolean };
/* eslint-disable ts/method-signature-style */
/** /**
* Used to store account data. * Used to store account data.
*/ */
@ -20,16 +19,18 @@ export interface AccountStore {
/** /**
* Finds the setting of the account with the given identifier. * Finds the setting of the account with the given identifier.
*
* @param id - The account identifier. * @param id - The account identifier.
* @param setting - The setting to find the value of. * @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. * Updates the settings for the account with the given identifier to the new values.
*
* @param id - The account identifier. * @param id - The account identifier.
* @param setting - The setting to update. * @param setting - The setting to update.
* @param value - The new value for the setting. * @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>;
} }

View File

@ -22,9 +22,10 @@ export class BaseAccountStore extends Initializer implements AccountStore {
private readonly storage: AccountLoginStorage<{ [ACCOUNT_TYPE]: typeof ACCOUNT_STORAGE_DESCRIPTION }>; private readonly storage: AccountLoginStorage<{ [ACCOUNT_TYPE]: typeof ACCOUNT_STORAGE_DESCRIPTION }>;
private initialized = false; private initialized = false;
public constructor(storage: AccountLoginStorage<any>) { // Wrong typings to prevent Components.js typing issues
public constructor(storage: AccountLoginStorage<Record<string, never>>) {
super(); super();
this.storage = storage as typeof this.storage; this.storage = storage as unknown as typeof this.storage;
} }
// Initialize the type definitions // Initialize the type definitions

View File

@ -23,7 +23,7 @@ export class BaseCookieStore implements CookieStore {
} }
public async get(cookie: string): Promise<string | undefined> { 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> { public async refresh(cookie: string): Promise<Date | undefined> {

View File

@ -61,7 +61,7 @@ export class BaseLoginAccountStorage<T extends IndexTypeCollection<T>> implement
return this.storage.defineType(type, description); 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); 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[]> { 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> { public async set<TType extends StringKey<T>>(type: TType, value: TypeObject<T[TType]>): Promise<void> {

View File

@ -5,6 +5,7 @@ export interface CookieStore {
/** /**
* Generates and stores a new cookie for the given accountId. * Generates and stores a new cookie for the given accountId.
* This does not replace previously generated cookies. * This does not replace previously generated cookies.
*
* @param accountId - Account to create a cookie for. * @param accountId - Account to create a cookie for.
* *
* @returns The generated cookie. * @returns The generated cookie.
@ -13,18 +14,21 @@ export interface CookieStore {
/** /**
* Return the accountID associated with the given cookie. * Return the accountID associated with the given cookie.
*
* @param cookie - Cookie to find the account for. * @param cookie - Cookie to find the account for.
*/ */
get: (cookie: string) => Promise<string | undefined>; get: (cookie: string) => Promise<string | undefined>;
/** /**
* Refreshes the cookie expiration and returns when it will expire if the cookie exists. * Refreshes the cookie expiration and returns when it will expire if the cookie exists.
*
* @param cookie - Cookie to refresh. * @param cookie - Cookie to refresh.
*/ */
refresh: (cookie: string) => Promise<Date | undefined>; refresh: (cookie: string) => Promise<Date | undefined>;
/** /**
* Deletes the given cookie. * Deletes the given cookie.
*
* @param cookie - Cookie to delete. * @param cookie - Cookie to delete.
*/ */
delete: (cookie: string) => Promise<boolean>; delete: (cookie: string) => Promise<boolean>;

View File

@ -27,9 +27,10 @@ export class BaseClientCredentialsStore extends Initializer implements ClientCre
private initialized = false; private initialized = false;
public constructor(storage: AccountLoginStorage<any>) { // Wrong typings to prevent Components.js typing issues
public constructor(storage: AccountLoginStorage<Record<string, never>>) {
super(); super();
this.storage = storage as typeof this.storage; this.storage = storage as unknown as typeof this.storage;
} }
// Initialize the type definitions // Initialize the type definitions

View File

@ -1,10 +1,11 @@
import { RepresentationMetadata } from '../../../http/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../http/representation/RepresentationMetadata';
import { getLoggerFor } from '../../../logging/LogUtil'; import { getLoggerFor } from '../../../logging/LogUtil';
import type { Json } from '../../../util/Json';
import { SOLID_HTTP } from '../../../util/Vocabularies'; import { SOLID_HTTP } from '../../../util/Vocabularies';
import { ACCOUNT_SETTINGS_REMEMBER_LOGIN } from '../account/util/AccountStore'; import { ACCOUNT_SETTINGS_REMEMBER_LOGIN } from '../account/util/AccountStore';
import type { AccountStore } from '../account/util/AccountStore'; import type { AccountStore } from '../account/util/AccountStore';
import type { CookieStore } from '../account/util/CookieStore'; import type { CookieStore } from '../account/util/CookieStore';
import type { Json, JsonRepresentation } from '../InteractionUtil'; import type { JsonRepresentation } from '../InteractionUtil';
import { finishInteraction } from '../InteractionUtil'; import { finishInteraction } from '../InteractionUtil';
import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler'; import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
import { JsonInteractionHandler } 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 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. * @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. * Takes the necessary steps to log a user in.
*
* @param input - Same input that was passed to the handle function. * @param input - Same input that was passed to the handle function.
*/ */
public abstract login(input: JsonInteractionHandlerInput): Promise<JsonRepresentation<LoginOutputType>>; public abstract login(input: JsonInteractionHandlerInput): Promise<JsonRepresentation<LoginOutputType>>;

View File

@ -29,9 +29,10 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
private readonly saltRounds: number; private readonly saltRounds: number;
private initialized = false; 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(); super();
this.storage = storage as typeof this.storage; this.storage = storage as unknown as typeof this.storage;
this.saltRounds = saltRounds; this.saltRounds = saltRounds;
} }

View File

@ -6,7 +6,9 @@ export interface ForgotPasswordStore {
* Creates a Forgot Password Confirmation Record. This will be to remember that * 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 * a user has made a request to reset a password. Throws an error if the email doesn't
* exist. * exist.
*
* @param id - ID of the email/password login object. * @param id - ID of the email/password login object.
*
* @returns The record id. This should be included in the reset password link. * @returns The record id. This should be included in the reset password link.
*/ */
generate: (id: string) => Promise<string>; generate: (id: string) => Promise<string>;
@ -14,13 +16,16 @@ export interface ForgotPasswordStore {
/** /**
* Gets the email associated with the forgot password confirmation record * Gets the email associated with the forgot password confirmation record
* or undefined if it's not present. * or undefined if it's not present.
*
* @param recordId - The record id retrieved from the link. * @param recordId - The record id retrieved from the link.
*
* @returns The user's email. * @returns The user's email.
*/ */
get: (recordId: string) => Promise<string | undefined>; get: (recordId: string) => Promise<string | undefined>;
/** /**
* Deletes the Forgot Password Confirmation Record. * Deletes the Forgot Password Confirmation Record.
*
* @param recordId - The record id of the forgot password confirmation record. * @param recordId - The record id of the forgot password confirmation record.
*/ */
delete: (recordId: string) => Promise<boolean>; delete: (recordId: string) => Promise<boolean>;

View File

@ -44,9 +44,10 @@ export class BasePodStore extends Initializer implements PodStore {
private initialized = false; 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(); super();
this.storage = storage as typeof this.storage; this.storage = storage as unknown as typeof this.storage;
this.visible = visible; this.visible = visible;
this.manager = manager; this.manager = manager;
} }

View File

@ -10,7 +10,7 @@ import type { InteractionRoute } from './InteractionRoute';
* Rejects operations that target a different route, * Rejects operations that target a different route,
* otherwise the input parameters are passed to the source handler. * 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 route: T;
protected readonly source: JsonInteractionHandler; protected readonly source: JsonInteractionHandler;

View File

@ -23,9 +23,10 @@ export class BaseWebIdStore extends Initializer implements WebIdStore {
private readonly storage: AccountLoginStorage<{ [WEBID_STORAGE_TYPE]: typeof WEBID_STORAGE_DESCRIPTION }>; private readonly storage: AccountLoginStorage<{ [WEBID_STORAGE_TYPE]: typeof WEBID_STORAGE_DESCRIPTION }>;
private initialized = false; private initialized = false;
public constructor(storage: AccountLoginStorage<any>) { // Wrong typings to prevent Components.js typing issues
public constructor(storage: AccountLoginStorage<Record<string, never>>) {
super(); super();
this.storage = storage as typeof this.storage; this.storage = storage as unknown as typeof this.storage;
} }
// Initialize the type definitions // Initialize the type definitions

View File

@ -81,6 +81,7 @@ export class ClientIdAdapter extends PassthroughAdapter {
/** /**
* Parses RDF data found at a Client ID. * Parses RDF data found at a Client ID.
*
* @param data - Raw data from the Client ID. * @param data - Raw data from the Client ID.
* @param id - The actual Client ID. * @param id - The actual Client ID.
* @param response - Response object from the request. * @param response - Response object from the request.

View File

@ -100,6 +100,7 @@ export * from './http/output/error/ConvertingErrorHandler';
export * from './http/output/error/ErrorHandler'; export * from './http/output/error/ErrorHandler';
export * from './http/output/error/RedirectingErrorHandler'; export * from './http/output/error/RedirectingErrorHandler';
export * from './http/output/error/SafeErrorHandler'; export * from './http/output/error/SafeErrorHandler';
export * from './http/output/error/TargetExtractorErrorHandler';
// HTTP/Output/Metadata // HTTP/Output/Metadata
export * from './http/output/metadata/AllowAcceptHeaderWriter'; 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/WebSocketMap';
export * from './server/notifications/WebSocketChannel2023/WebSocketChannel2023Type'; 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 // Server/Notifications
export * from './server/notifications/ActivityEmitter'; export * from './server/notifications/ActivityEmitter';
export * from './server/notifications/BaseChannelType'; export * from './server/notifications/BaseChannelType';
@ -615,6 +624,7 @@ export * from './util/GenericEventEmitter';
export * from './util/GuardedStream'; export * from './util/GuardedStream';
export * from './util/HeaderUtil'; export * from './util/HeaderUtil';
export * from './util/IterableUtil'; export * from './util/IterableUtil';
export * from './util/Json';
export * from './util/PathUtil'; export * from './util/PathUtil';
export * from './util/PromiseUtil'; export * from './util/PromiseUtil';
export * from './util/QuadUtil'; export * from './util/QuadUtil';

View File

@ -1,4 +1,3 @@
/* eslint-disable unicorn/no-process-exit */
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import type { WriteStream } from 'node:tty'; import type { WriteStream } from 'node:tty';
import type { IComponentsManagerBuilderOptions } from 'componentsjs'; import type { IComponentsManagerBuilderOptions } from 'componentsjs';
@ -105,9 +104,9 @@ export class AppRunner {
let configs = input.config ?? [ '@css:config/default.json' ]; let configs = input.config ?? [ '@css:config/default.json' ];
configs = (Array.isArray(configs) ? configs : [ configs ]).map(resolveAssetPath); configs = (Array.isArray(configs) ? configs : [ configs ]).map(resolveAssetPath);
let componentsManager: ComponentsManager<any>; let componentsManager: ComponentsManager<App | CliResolver>;
try { try {
componentsManager = await this.createComponentsManager<any>(loaderProperties, configs); componentsManager = await this.createComponentsManager<App>(loaderProperties, configs);
} catch (error: unknown) { } catch (error: unknown) {
this.resolveError(`Could not build the config files from ${configs.join(',')}`, error); 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 { public runCliSync({ argv, stderr = process.stderr }: { argv?: CliArgv; stderr?: WriteStream }): void {
this.runCli(argv).catch((error): never => { this.runCli(argv).catch((error): never => {
stderr.write(createErrorMessage(error)); stderr.write(createErrorMessage(error));
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1); process.exit(1);
}); });
} }
@ -203,6 +203,7 @@ export class AppRunner {
/** /**
* Retrieves settings from package.json or configuration file when * Retrieves settings from package.json or configuration file when
* part of an npm project. * part of an npm project.
*
* @returns The settings defined in the configuration file * @returns The settings defined in the configuration file
*/ */
public async getPackageSettings(): Promise<undefined | Record<string, unknown>> { public async getPackageSettings(): Promise<undefined | Record<string, unknown>> {
@ -225,9 +226,9 @@ export class AppRunner {
return import(cssConfigPathJs) as Promise<Record<string, unknown>>; 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 // 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') { if (typeof pkg.config?.['community-solid-server'] === 'object') {
return pkg.config['community-solid-server'] as Record<string, unknown>; return pkg.config['community-solid-server'] as Record<string, unknown>;
} }

View File

@ -14,6 +14,7 @@ import { Initializer } from './Initializer';
* Part of the dynamic pod creation. * Part of the dynamic pod creation.
* Reads the contents from the configuration storage, uses those values to instantiate ResourceStores, * Reads the contents from the configuration storage, uses those values to instantiate ResourceStores,
* and then adds them to the routing storage. * and then adds them to the routing storage.
*
* @see {@link ConfigPodManager}, {@link TemplatedPodGenerator}, {@link BaseUrlRouterRule} * @see {@link ConfigPodManager}, {@link TemplatedPodGenerator}, {@link BaseUrlRouterRule}
*/ */
export class ConfigPodInitializer extends Initializer { export class ConfigPodInitializer extends Initializer {

View File

@ -1,5 +1,4 @@
import type { Server } from 'node:http'; import type { Server } from 'node:http';
import { URL } from 'node:url';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import { isHttpsServer } from '../server/HttpServerFactory'; import { isHttpsServer } from '../server/HttpServerFactory';

View File

@ -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 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} * @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.name = name;
this.options = options; this.options = options;
} }
@ -76,8 +76,9 @@ export class YargsCliExtractor extends CliExtractor {
yArgv.check((args): boolean => { yArgv.check((args): boolean => {
for (const [ name, options ] of Object.entries(this.yargsArgOptions)) { for (const [ name, options ] of Object.entries(this.yargsArgOptions)) {
if (options.type !== 'array' && Array.isArray(args[name])) { if (options.type !== 'array' && Array.isArray(args[name])) {
// eslint-disable-next-line ts/restrict-template-expressions throw new Error(
throw new Error(`Multiple values for --${name} (-${options.alias}) were provided where only one is allowed`); `Multiple values for --${name} (-${options.alias as string}) were provided where only one is allowed`,
);
} }
} }
return true; return true;

View File

@ -18,7 +18,9 @@ enum ClusterMode {
/** /**
* Convert workers amount to {@link ClusterMode} * Convert workers amount to {@link ClusterMode}
*
* @param workers - Amount of workers * @param workers - Amount of workers
*
* @returns ClusterMode enum value * @returns ClusterMode enum value
*/ */
function toClusterMode(workers: number): ClusterMode { function toClusterMode(workers: number): ClusterMode {
@ -92,6 +94,7 @@ export class ClusterManager {
/** /**
* Check whether the CSS server was booted in single threaded mode. * Check whether the CSS server was booted in single threaded mode.
*
* @returns True is single threaded. * @returns True is single threaded.
*/ */
public isSingleThreaded(): boolean { public isSingleThreaded(): boolean {
@ -100,6 +103,7 @@ export class ClusterManager {
/** /**
* Whether the calling process is the primary process. * Whether the calling process is the primary process.
*
* @returns True if primary * @returns True if primary
*/ */
public isPrimary(): boolean { public isPrimary(): boolean {
@ -108,6 +112,7 @@ export class ClusterManager {
/** /**
* Whether the calling process is a worker process. * Whether the calling process is a worker process.
*
* @returns True if worker * @returns True if worker
*/ */
public isWorker(): boolean { public isWorker(): boolean {

View File

@ -11,8 +11,10 @@ export interface SingleThreaded {}
/** /**
* Convert an exported interface name to the properly expected Components.js type URI. * Convert an exported interface name to the properly expected Components.js type URI.
*
* @param componentsManager - The currently used ComponentsManager * @param componentsManager - The currently used ComponentsManager
* @param interfaceName - An interface name * @param interfaceName - An interface name
*
* @returns A Components.js type URI * @returns A Components.js type URI
*/ */
export async function toComponentsJsType<T>(componentsManager: ComponentsManager<T>, interfaceName: string): 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} * Will list class names of components instantiated implementing the {@link SingleThreaded}
* interface while the application is being run in multithreaded mode. * interface while the application is being run in multithreaded mode.
*
* @param componentsManager - The componentsManager being used to set up the application * @param componentsManager - The componentsManager being used to set up the application
*/ */
export async function listSingleThreadedComponents<T>(componentsManager: ComponentsManager<T>): Promise<string[]> { export async function listSingleThreadedComponents<T>(componentsManager: ComponentsManager<T>): Promise<string[]> {

View File

@ -68,11 +68,13 @@ export interface V6MigrationInitializerArgs {
/** /**
* Storages for which all entries need to be removed. * Storages for which all entries need to be removed.
*/ */
// eslint-disable-next-line ts/no-explicit-any
cleanupStorages: KeyValueStorage<string, any>[]; cleanupStorages: KeyValueStorage<string, any>[];
/** /**
* The storage that will contain the account data in the new format. * 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. * 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 accountStorage: KeyValueStorage<string, Account | Settings>;
private readonly clientCredentialsStorage: KeyValueStorage<string, ClientCredentials>; 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 newAccountStorage: AccountLoginStorage<typeof STORAGE_DESCRIPTION>;
private readonly newSetupStorage: KeyValueStorage<string, string>; private readonly newSetupStorage: KeyValueStorage<string, string>;
@ -113,7 +115,7 @@ export class V6MigrationInitializer extends Initializer {
this.accountStorage = args.accountStorage; this.accountStorage = args.accountStorage;
this.clientCredentialsStorage = args.clientCredentialsStorage; this.clientCredentialsStorage = args.clientCredentialsStorage;
this.cleanupStorages = args.cleanupStorages; 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; this.newSetupStorage = args.newSetupStorage;
} }
@ -147,7 +149,7 @@ export class V6MigrationInitializer extends Initializer {
].join(' '), resolve); ].join(' '), resolve);
}); });
readline.close(); readline.close();
if (!/^y(?:es)?$/ui.test(answer)) { if (!/^y(?:es)?$/iu.test(answer)) {
throw new Error('Stopping server as migration was cancelled.'); throw new Error('Stopping server as migration was cancelled.');
} }
} }

View File

@ -14,7 +14,7 @@ export class CombinedShorthandResolver extends ShorthandResolver {
} }
public async handle(input: Record<string, unknown>): Promise<Record<string, unknown>> { 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)) { for (const [ name, computer ] of Object.entries(this.resolvers)) {
try { try {
vars[name] = await computer.handleSafe(input); vars[name] = await computer.handleSafe(input);

View File

@ -42,6 +42,7 @@ export function getLoggerFor(loggable: string | Instance): Logger {
/** /**
* Sets the global logger factory. * Sets the global logger factory.
* This causes loggers created by {@link getLoggerFor} to delegate to a logger from the given factory. * This causes loggers created by {@link getLoggerFor} to delegate to a logger from the given factory.
*
* @param loggerFactory - A logger factory. * @param loggerFactory - A logger factory.
*/ */
export function setGlobalLoggerFactory(loggerFactory: LoggerFactory): void { export function setGlobalLoggerFactory(loggerFactory: LoggerFactory): void {

View File

@ -1,5 +1,4 @@
import cluster from 'node:cluster'; import cluster from 'node:cluster';
import process from 'node:process';
import type { LogLevel } from './LogLevel'; import type { LogLevel } from './LogLevel';
export interface LogMetadata { export interface LogMetadata {
@ -18,6 +17,7 @@ export interface SimpleLogger {
/** /**
* Log the given message at the given level. * Log the given message at the given level.
* If the internal level is higher than the given level, the message may be voided. * If the internal level is higher than the given level, the message may be voided.
*
* @param level - The level to log at. * @param level - The level to log at.
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @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. * Log the given message at the given level.
* If the internal level is higher than the given level, the message may be voided. * If the internal level is higher than the given level, the message may be voided.
*
* @param level - The level to log at. * @param level - The level to log at.
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @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. * Log a message at the 'error' level.
*
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @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. * Log a message at the 'warn' level.
*
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @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. * Log a message at the 'info' level.
*
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @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. * Log a message at the 'verbose' level.
*
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @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. * Log a message at the 'debug' level.
*
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @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. * Log a message at the 'silly' level.
*
* @param message - The message to log. * @param message - The message to log.
* @param meta - Optional metadata to include in the log message. * @param meta - Optional metadata to include in the log message.
*/ */

View File

@ -6,6 +6,7 @@ import type { Logger } from './Logger';
export interface LoggerFactory { export interface LoggerFactory {
/** /**
* Create a logger instance for the given label. * Create a logger instance for the given label.
*
* @param label - A label that is used to identify the given logger. * @param label - A label that is used to identify the given logger.
*/ */
createLogger: (label: string) => Logger; createLogger: (label: string) => Logger;

View File

@ -13,7 +13,7 @@ export class WinstonLogger extends BaseLogger {
this.logger = logger; 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); this.logger.log(level, message, meta);
return this; return this;
} }

View File

@ -1,3 +1,4 @@
import type { TransformableInfo } from 'logform';
import { createLogger, format, transports } from 'winston'; import { createLogger, format, transports } from 'winston';
import type * as Transport from 'winston-transport'; import type * as Transport from 'winston-transport';
import type { Logger, LogMetadata } from './Logger'; import type { Logger, LogMetadata } from './Logger';
@ -33,7 +34,7 @@ export class WinstonLoggerFactory implements LoggerFactory {
format.timestamp(), format.timestamp(),
format.metadata({ fillExcept: [ 'level', 'timestamp', 'label', 'message' ]}), format.metadata({ fillExcept: [ 'level', 'timestamp', 'label', 'message' ]}),
format.printf( 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}`, `${timestamp} [${labelInner}] {${this.clusterInfo(meta as LogMetadata)}} ${levelInner}: ${message}`,
), ),
), ),

View File

@ -7,6 +7,7 @@ import type { PodSettings } from './settings/PodSettings';
export interface PodManager { export interface PodManager {
/** /**
* Creates a pod for the given settings. * Creates a pod for the given settings.
*
* @param settings - Settings describing the pod. * @param settings - Settings describing the pod.
* @param overwrite - If the creation should proceed if there already is a resource there. * @param overwrite - If the creation should proceed if there already is a resource there.
*/ */

View File

@ -10,7 +10,7 @@ import type { ComponentsJsFactory } from './ComponentsJsFactory';
* but moduleState will be stored in between calls. * but moduleState will be stored in between calls.
*/ */
export class BaseComponentsJsFactory implements ComponentsJsFactory { export class BaseComponentsJsFactory implements ComponentsJsFactory {
private readonly options: IComponentsManagerBuilderOptions<any>; private readonly options: IComponentsManagerBuilderOptions<unknown>;
public constructor(relativeModulePath = '../../../', logLevel = 'error') { public constructor(relativeModulePath = '../../../', logLevel = 'error') {
this.options = { 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); const manager = await ComponentsManager.build(this.options);
this.options.moduleState = manager.moduleState; this.options.moduleState = manager.moduleState;
return manager; return manager;
@ -29,16 +29,17 @@ export class BaseComponentsJsFactory implements ComponentsJsFactory {
/** /**
* Calls Components.js to instantiate a new object. * Calls Components.js to instantiate a new object.
*
* @param configPath - Location of the config to instantiate. * @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 * @param variables - Variables to send to Components.js
* *
* @returns The resulting object, corresponding to the given component IRI. * @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> { Promise<T> {
const manager = await this.buildManager(); const manager = await this.buildManager();
await manager.configRegistry.register(configPath); await manager.configRegistry.register(configPath);
return await manager.instantiate(componentIri, { variables }); return manager.instantiate(componentIri, { variables });
} }
} }

Some files were not shown because too many files have changed in this diff Show More