├── .github └── workflows │ ├── main.yml │ └── mkdocs.yml ├── .gitignore ├── .npmignore ├── .release-it.json ├── CPU.20250424.163833.322778.0.001.cpuprofile ├── LICENSE.md ├── README.md ├── bin ├── bundled.js ├── solid-dev.js └── solid.js ├── dexa.png ├── documentation ├── markdown │ ├── README.md │ ├── contributing │ │ └── making-changes.md │ ├── documentation │ │ ├── cli │ │ │ ├── access.md │ │ │ ├── aliases.md │ │ │ ├── authentication.md │ │ │ └── commands.md │ │ ├── overview.md │ │ ├── setup.md │ │ └── typescript │ │ │ ├── authentication.md │ │ │ ├── css-specific.md │ │ │ ├── example-requests.md │ │ │ ├── metadata.md │ │ │ └── overview.md │ └── tutorial.md ├── mkdocs.yml ├── overrides │ └── main.html └── typedoc.css ├── node_trace.1.log ├── package-lock.json ├── package.json ├── src ├── authentication │ ├── AuthenticationInteractive.ts │ ├── AuthenticationToken.ts │ ├── CreateFetch.ts │ ├── TokenCreationCSS.ts │ └── authenticate.ts ├── commands │ ├── solid-command.ts │ ├── solid-copy.ts │ ├── solid-edit.ts │ ├── solid-fetch.ts │ ├── solid-find.ts │ ├── solid-list.ts │ ├── solid-mkdir.ts │ ├── solid-move.ts │ ├── solid-perms.ts │ ├── solid-perms_acl.ts │ ├── solid-pod-create.ts │ ├── solid-query.ts │ ├── solid-remove.ts │ ├── solid-shell.ts │ ├── solid-touch.ts │ └── solid-tree.ts ├── index.ts ├── logger.ts ├── shell │ ├── commands │ │ ├── SolidCommand.ts │ │ ├── auth.ts │ │ ├── copy.ts │ │ ├── css-specific │ │ │ └── create-pod.ts │ │ ├── edit.ts │ │ ├── exit.ts │ │ ├── fetch.ts │ │ ├── find.ts │ │ ├── list.ts │ │ ├── mkdir.ts │ │ ├── mv.ts │ │ ├── navigation │ │ │ ├── cd.ts │ │ │ └── cs.ts │ │ ├── perms.ts │ │ ├── perms_acl.ts │ │ ├── query.ts │ │ ├── remove.ts │ │ ├── shell.ts │ │ ├── touch.ts │ │ └── tree.ts │ └── shellcommands.ts └── utils │ ├── authenticationUtils.ts │ ├── configoptions.ts │ ├── errors │ └── BashlibError.ts │ ├── shellutils.ts │ ├── userInteractions.ts │ └── util.ts ├── tsconfig.json └── typedoc.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | - "master" 7 | - "versions/*" 8 | tags: 9 | - "v*" 10 | pull_request: 11 | 12 | jobs: 13 | mkdocs-release: 14 | uses: ./.github/workflows/mkdocs.yml 15 | -------------------------------------------------------------------------------- /.github/workflows/mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | workflow_call: 4 | 5 | # Additional trigger to deploy changes to the documentation/ folder 6 | # on push to main, ignoring tags so we don't trigger twice upon release 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - documentation/** 12 | tags-ignore: 13 | - "*" 14 | 15 | jobs: 16 | mkdocs-prep: 17 | # Runs the markdown linter to ensure we don't release faulty markdown. 18 | # Also gets the correct major version, whether the job is triggered by a version tag 19 | # or a push to main to update the latest documentation. 20 | runs-on: ubuntu-latest 21 | outputs: 22 | major: ${{ steps.tagged_version.outputs.major || steps.current_version.outputs.major }} 23 | steps: 24 | - uses: actions/checkout@v3.4.0 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: "16.x" 28 | - run: npm ci --ignore-scripts 29 | - if: startsWith(github.ref, 'refs/tags/v') 30 | name: Get tagged version 31 | id: tagged_version 32 | uses: battila7/get-version-action@v2 33 | - if: github.ref == 'refs/heads/main' 34 | name: Get current version 35 | id: current_version 36 | run: | 37 | VERSION=$(git show origin/main:package.json | jq -r .version | grep -Po '^(\d+)') 38 | echo "major=$VERSION" >> $GITHUB_OUTPUT 39 | mkdocs: 40 | runs-on: ubuntu-latest 41 | needs: mkdocs-prep 42 | steps: 43 | - uses: actions/checkout@v3.4.0 44 | - uses: actions/setup-python@v4 45 | with: 46 | python-version: 3.x 47 | - run: pip install mkdocs-material 48 | - run: pip install mike 49 | - run: git config user.name ci-bot 50 | - run: git config user.email ci-bot@example.com 51 | - run: git fetch origin gh-pages --depth=1 52 | - run: | 53 | cd documentation && mike deploy --push --update-aliases \ 54 | typedocs latest 55 | 56 | typedocs: 57 | # Build typedocs and publish them to the GH page. 58 | # `mike deploy` overwrites the entire folder for a version so these need to be (re)built afterwards. 59 | needs: [mkdocs-prep, mkdocs] 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v3.4.0 63 | - uses: actions/setup-node@v3 64 | with: 65 | node-version: "16.x" 66 | - run: npm ci --ignore-scripts 67 | - name: Generate typedocs 68 | run: npm run typedocs 69 | - name: Deploy typedocs 70 | uses: peaceiris/actions-gh-pages@v3 71 | with: 72 | github_token: ${{ secrets.GITHUB_TOKEN }} 73 | publish_dir: ./docs 74 | destination_dir: ${{ needs.mkdocs-prep.outputs.major }}.x/docs 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.eslintcache 2 | /dist 3 | /docs 4 | /node_modules 5 | /documentation/site/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.eslintcache 2 | /docs 3 | /node_modules 4 | /.github 5 | /documentation -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2022–2025 Inrupt Inc. and imec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bashlib 2 | 3 | **For [documentation](https://solidlabresearch.github.io/Bashlib/documentation/overview/) and a [tutorial](https://solidlabresearch.github.io/Bashlib/tutorial/), 4 | please visit the [Bashlib Website](https://solidlabresearch.github.io/Bashlib/).** 5 | 6 | 7 | The Bashlib-solid library provides a set of functions for interacting with Solid environments from Node.JS and the CLI. 8 | The aim is to provide shell-like functionality to facility the use of and development for the Solid ecosystem with a low requirement of specific knowledge of Solid and LDP. 9 | 10 | This library makes use of the [Developer tools by inrupt for Solid](https://docs.inrupt.com/developer-tools/javascript/client-libraries/using-libraries/) to support authorization, authentication and resource loading. 11 | To support querying requirements, this library makes use of the [Comunica Query engine](https://comunica.dev/). 12 | 13 | ### Bashlib features in progress 14 | 15 | - [X] Improve token management 16 | - [X] Improve session management 17 | - [X] Handle metadata 18 | - [ ] Handling multiple pods for a given WebID (pim:storage) 19 | - [X] multi parameter removes: rm file1 file2 file3 20 | - [ ] Fixing session refresh. Current implementation can have time-out isssues with longer commands. 21 | - [ ] Make sure discovery of pim:storage and ldp:inbox are according to spec! 22 | - [X] Resource verification on edit (compare before / after hash and notify if something may have changed) 23 | - [ ] Write concrete test cases and spin up local pod server to test 24 | - [ ] Improve consistency of internal logging 25 | - [ ] Improve consistency of exported Javascript interface 26 | - [ ] Add WebID parameter to force using specific webid 27 | - [ ] Add output parameter to log to specified file 28 | - [ ] Improve error handling messaging 29 | - [X] npm release 30 | - [ ] Refactor to use components.js for dynamic extension with new utilities 31 | 32 | -------------------------------------------------------------------------------- /bin/solid-dev.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const { Command } = require('commander'); 4 | let program = new Command(); 5 | 6 | const c = require('../dist/shell/shellcommands') 7 | const initConfig = require('../dist/utils/configoptions').initializeConfig 8 | 9 | // Fix for console error in Inrupt lib. 10 | let consoleErrorFunction = console.error; 11 | console.error = function(errorString){ 12 | if (errorString && !errorString.includes('DraftWarning') && !errorString.includes('ExperimentalWarning')) { 13 | consoleErrorFunction(errorString) 14 | } 15 | }; 16 | 17 | initConfig(); 18 | 19 | program 20 | .name('solid-dev') 21 | .description('Utility toolings for the Community Solid Server.') 22 | .version('0.2.0') 23 | 24 | program = new c.CreatePodCommand(undefined, true).addCommand(program) 25 | 26 | program 27 | .parse(process.argv); 28 | -------------------------------------------------------------------------------- /bin/solid.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const { Command } = require('commander'); 4 | let program = new Command(); 5 | 6 | const c = require('../dist/shell/shellcommands') 7 | const initConfig = require('../dist/utils/configoptions').initializeConfig 8 | 9 | // Fix for console error in Inrupt lib. 10 | let consoleErrorFunction = console.error; 11 | console.error = function(errorString){ 12 | if (errorString && !errorString.includes('DraftWarning') && !errorString.includes('ExperimentalWarning')) { 13 | consoleErrorFunction(errorString) 14 | } 15 | }; 16 | 17 | initConfig(); 18 | 19 | program 20 | .name('solid') 21 | .description('Utility toolings for interacting with a Solid server.') 22 | .version('0.6.5') 23 | .enablePositionalOptions() 24 | .option('-a, --auth ', 'token | interactive | request | none - Authentication type (defaults to "request")') 25 | // .option('-i, --idp ', 'URL of the Solid Identity Provider') 26 | // .option('-c, --config ', 'Location of config file with authentication info.') 27 | .option('--port', 'Specify port to be used when redirecting in Solid authentication flow. Defaults to 3435.') 28 | 29 | program = new c.FetchCommand(undefined, true).addCommand(program) 30 | program = new c.ListCommand(undefined, true).addCommand(program) 31 | program = new c.TreeCommand(undefined, true).addCommand(program) 32 | program = new c.CopyCommand(undefined, true).addCommand(program) 33 | program = new c.MoveCommand(undefined, true).addCommand(program) 34 | program = new c.RemoveCommand(undefined, true).addCommand(program) 35 | program = new c.TouchCommand(undefined, true).addCommand(program) 36 | program = new c.MkdirCommand(undefined, true).addCommand(program) 37 | program = new c.FindCommand(undefined, true).addCommand(program) 38 | program = new c.QueryCommand(undefined, true).addCommand(program) 39 | program = new c.PermsCommand(undefined, true).addCommand(program) 40 | program = new c.EditCommand(undefined, true).addCommand(program) 41 | // program = new c.ShellCommand(undefined, true).addCommand(program) 42 | // program = new c.ExitCommand(undefined, true).addCommand(program) 43 | program = new c.AuthCommand(undefined, true).addCommand(program) 44 | 45 | program 46 | .parse(process.argv); 47 | 48 | -------------------------------------------------------------------------------- /dexa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidLabResearch/Bashlib/80de25cbb4b3ed057f95e25bc057f1be9b00cef3/dexa.png -------------------------------------------------------------------------------- /documentation/markdown/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | # Welcome 7 | 8 | Welcome to the Bashlib Homepage. 9 | 10 | Bashlib is a Command Line Interface for working with Solid Pods, made by 11 | Ruben Dedecker at the 12 | KNoWS team at Ghent University. 13 | 14 | An introduction to quickly setting up a Community Solid Server instance and 15 | interfacing with it using Bashlib is found in the tutorial section. 16 | 17 | Documentation on the tool and the available commands is found in 18 | the documentation section. 19 | 20 | Documentation may be incomplete in content and structure. 21 | Feel free to open a [discussion](https://github.com/SolidLabResearch/Bashlib/issues/) 22 | and report incorrect information. 23 | -------------------------------------------------------------------------------- /documentation/markdown/contributing/making-changes.md: -------------------------------------------------------------------------------- 1 | # Pull Requests 2 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/cli/access.md: -------------------------------------------------------------------------------- 1 | # Access Management 2 | The `access` command is used to manage access of resources on a Solid pod. 3 | Solid has two competing authorization proposals, Web Access Controls that use `.acl` resources, 4 | and Access Control Policies that use `.acp` resources. 5 | 6 | Bashlib implements full support for the management of WAC resources, and partial support for the management of ACP resources using the Inrupt universalAccess libraries. 7 |
8 | The access command has thee subcommands: `list`, `set` and `delete` 9 | 10 | ## List 11 | The `list` subcommand provides a listing of all access information for the targeted (container) resource. 12 | 13 | #### arguments 14 | ``` 15 | Arguments: 16 | url Resource URL 17 | ``` 18 | The `url` argument is the target (container) resource for which access is to be listed. 19 | 20 | #### options 21 | ``` 22 | Options: 23 | -p, --pretty Pretty format 24 | -v, --verbose Log all operations 25 | ``` 26 | Depending on if the target Solid pod is managed using the `WAC` or `ACP` 27 | authorization system, options such as showing `default` access indicating 28 | that the authorization is recursively enforced on child resources without their own `.acl` file 29 | or `inhereted` access indicating that the access rules are derived from the default access of a parent resource 30 | will be restricted to `WAC` based Solid servers. 31 |
32 | The `--pretty` option outputs the information in a table format 33 |
34 | The `--verbose` option outputs operation logs. 35 | 36 | 37 | #### examples 38 | List the pod root access in a pretty format 39 | ``` 40 | sld access list --pretty https://mypod.org/ 41 | ``` 42 | 43 | ## Set 44 | The `set` subcommand is used to edit resource access. 45 | 46 | #### arguments 47 | ``` 48 | Arguments: 49 | url Resource URL 50 | permissions Permission format when setting permissions. 51 | Format according to id=[a][c][r][w]. 52 | For public permissions please set id to "p". 53 | For the current authenticated user please set id to "u". 54 | For specific agents, set id to be the agent webid. 55 | ``` 56 | The `url` argument is the target (container) resource for editing access rules. 57 |
58 | The `permissions` argument is a formatted string containing the identifier for 59 | which rules are defined, and the associated permissions that are to be set for the 60 | given identifier. Using `p` as the identifier targets public permissions and using `u` 61 | as the identifier targets the current WebID of the authenticated Bashlib session. 62 |
63 | The `a` is append rights, allowing PATCH operations to be made. 64 |
65 | The `c` is control rights, allowing the editing of access controls for a resource (for ACP this includes both readControl and writeControl) 66 |
67 | The `r` is read rights, allowing a GET request to a resource. 68 |
69 | The `p` is write rights. For a resource this allows it to be overwritten using a PUT request. 70 | For a container this allows resources to be added using both PUT and POST requests. 71 | 72 | #### options 73 | ``` 74 | Options: 75 | --default Set the defined permissions as default (only when target pod is hosted on a WAC-based instance) 76 | --group Process identifier as a group identifier (only when target pod is hosted on a WAC-based instance) 77 | -v, --verbose Log all operations 78 | -h, --help display help for command 79 | ``` 80 | The `--default` option makes the current access rules default for all children resources when defined on a container. Only available for pods hosted on a `WAC`-based Solid server. 81 |
82 | The `--group` option indicates that the identifier represents a group identifier. Only available for pods hosted on a `WAC`-based Solid server. 83 |
84 | The `--verbose` option outputs operation logs. 85 | 86 | #### examples 87 | Setting default public read permissions for a resource hosted on a WAC-based solid pod 88 | ``` 89 | sld access set https://mypod.org/resource p=r --default 90 | ``` 91 | 92 | Giving access to alice to write to a container 93 | ``` 94 | sld access set http://mypod.org/container/ http://people.org/alice/webid=w 95 | ``` 96 | 97 | Removing all public permissions from a resource (making it effectively private). 98 | Note that this will also remove any default permissions set on the resource. 99 | ``` 100 | sld access set https://mypod.org/resource p= 101 | ``` 102 | 103 | 104 | 105 | ## Delete 106 | The `delete` subcommand is only available for WAC based pods using `.acl` resources. 107 | Note that removing a resource using the `rm` command also removes the associated `.acl` resource on the CSS automatically. 108 | 109 | #### arguments 110 | ``` 111 | Arguments: 112 | url Resource URL 113 | ``` 114 | The `url` argument is the target `.acl` resource that will be deleted. 115 | 116 | #### options 117 | ``` 118 | Options: 119 | -v, --verbose Log all operations 120 | ``` 121 | The `--verbose` option output operation logs. 122 | 123 | ### example 124 | Removing an acl resource. 125 | ``` 126 | sld access remove https://mypod.org/resource.acl 127 | ``` 128 | 129 | 130 | #### examples 131 | 132 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/cli/aliases.md: -------------------------------------------------------------------------------- 1 | # Using aliases 2 | 3 | The current implementation of using aliases is weak, and may be changed in subsequent releases. 4 | Alias management is a potential future addition. 5 | 6 | ## Base 7 | The `base:` alias indicates the root of your Solid pod. 8 | Executing a command using this alias will target the root of your pod, if it is known. 9 | This value is taken from the `pim:storage` triple in the WebID. 10 | In case multiple storage locations are available, results in using this may be inconsistent. 11 |
12 | The following command wil make a listing of the root of the Solid pod of the current user. 13 | ``` 14 | sld ls base:/ 15 | ``` 16 | 17 | 18 | ## WebID 19 | The `webid:` alias will target the user WebID. 20 |
21 | The following command wil retrieve the user WebID. 22 | ``` 23 | sld curl webid: 24 | ``` 25 | 26 | ## Inbox 27 | The `inbox:` alias targets the user inbox, if known. 28 | This value is taken from the `ldp:inbox` triple in the WebID. 29 |
30 | The following command queries the inbox for events 31 | ``` 32 | sld query inbox:/ "Select ?event where { ?event a } 33 | ``` 34 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/cli/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication Management 2 | 3 | The Bashlib CLI interface provides multiple options for authentication management. 4 | It enables interactive login flows using the browser, that work with any Solid pod server. 5 | Additionally, it includes the client credentials flow to generate authentication tokens without needing a browser login 6 | both for the Community Solid Server v7 and the Enterprise Solid Server from Inrupt. 7 | 8 |
9 | All examples make use of the abstraction `sld` as an alias for `node bin/solid.js`, 10 | 11 | 12 | ## Enforcing specific authorization flows 13 | When setting up test flows on Solid, it might be interesting to force a specific authentication flows to be used. 14 | For this, the `--auth` option can be set on the bashlib program as such:] 15 | ``` 16 | sld --auth interactive 17 | ``` 18 | This examples forces authentication via an interactive browser session. Other options are `token` for only token based authentication, 19 | `none` for no authentication and `request` to dynamically choose an authentication option during use, which is the default. 20 | The `--port` option can be changed to change the port of the local service that is setup to manage interactive authentication flows with the browser. 21 | 22 | ## Auth command 23 | The `auth` command contains all functionality to manage authentication options and create client credentials tokens. 24 | 25 | ### Set 26 | The `set` subcommand is used to manage the authentication session for Bashlib. 27 | It provides the ability to set a specific WebID as an argument, or if no argument is given starts an interactive selection dialog to change the active WebID. 28 | 29 | #### arguments 30 | ``` 31 | Arguments: 32 | webid Set active WebID directly, without requiring manual selection. 33 | ``` 34 | The `webid` argument directly sets the session to the provided WebID value. 35 | 36 | #### examples 37 | Interactive session management 38 | ``` 39 | sld auth set 40 | ``` 41 | 42 | Setting a specific active WebID 43 | ``` 44 | sld auth set https://people.org/alice/webid 45 | ``` 46 | 47 | ### Show 48 | The `show` subcommand shows the current authentication session. It shows the WebId, if there is an active authentication session and if a client credential token is available. 49 | 50 | 51 | #### options 52 | ``` 53 | Options: 54 | -p, --pretty Show listing in table format. 55 | ``` 56 | The `--pretty` option displays the result in a table formate. 57 | 58 | #### examples 59 | ``` 60 | sld auth show 61 | ``` 62 | 63 | ### Clear 64 | The `clear` subcommand clears the current authentication session and active WebID. It does not remove any stored authentication information. 65 | 66 | #### examples 67 | ``` 68 | sld auth clear 69 | ``` 70 | 71 | 72 | ### List 73 | The `list` subcommand lists the stored authentication information. It shows the WebIds, if there is an active authentication session and if a client credential token is available. 74 | 75 | #### options 76 | ``` 77 | Options: 78 | -p, --pretty Show listing in table format. 79 | ``` 80 | The `--pretty` option displays the result in a table formate. 81 | 82 | #### examples 83 | ``` 84 | sld auth list 85 | ``` 86 | 87 | ### Remove 88 | The `remove` subcommand provides the ability to remove authentication information from Bashlib. 89 | It provides an interactive menu if no argument is given, or can remove information for specific WebIDs or all information directly via the CLI arguments. 90 | 91 | #### arguments 92 | ``` 93 | Arguments: 94 | string webid | all 95 | ``` 96 | The command has an optional parameter. 97 | When passing the argument string `all`, all authentication information is removed. 98 | Passing a specific WebID removes all authentication information tied to that WebID. 99 | If no argument is passed, an interactive CLI menu is provided. 100 | 101 | #### examples 102 | Opening the interactive menu 103 | ``` 104 | sld auth remove 105 | ``` 106 | 107 | Removing all authentication information 108 | ``` 109 | sld auth remove all 110 | ``` 111 | 112 | Removing a specific WebID 113 | ``` 114 | sld auth remove https://people.org/alice/webid 115 | ``` 116 | 117 | 118 | ### Create Token (CSS) 119 | The token creation is divided in two subcommands, 120 | one for the Community Solid Server and one for the Inrupt Enterprise Solid Server, 121 | as both have a different approach to token generation for client applications. 122 | 123 | The `create-token-css` command creates a client credentials token for pods hosted on 124 | a Community Solid Server version 7. The authentication options can be passed both 125 | as command line arguments, or in an interactive dialog if they are not provided through 126 | the CLI options. 127 | The interactive creation menu will ask to use the WebID of the current session to create 128 | a token when available. 129 | 130 | #### options 131 | ``` 132 | Options: 133 | -w, --webid User WebID 134 | -n, --name Token name 135 | -e, --email User email 136 | -p, --password User password 137 | -v, --verbose Log actions 138 | -h, --help display help for command 139 | ``` 140 | The `--webid` option is the WebID for which the token is created. 141 |
142 | The `--name` option is the name of the token (only important for token management). 143 |
144 | The `--email` option is the email that was used to create the account tied to the WebID. 145 |
146 | The `--password` option is the password tied to the account. 147 |
148 | the `--verbose` option outputs operation logs. 149 | 150 | #### examples 151 | Open interactive dialog to create token 152 | ``` 153 | sld auth create-token-css 154 | ``` 155 | 156 | ### Create Token (ESS) 157 | The `create-token-ess` command creates a client credentials token for pods hosted on 158 | an Inrupt Enterprise Solid Server. The authentication options can be passed both 159 | as command line arguments, or in an interactive dialog if they are not provided through 160 | the CLI options. 161 | The interactive creation menu will ask to use the WebID of the current session to create 162 | a token when available. 163 |
164 | The Inrupt token generation relies on the registration of applications via their 165 | application registration service. 166 | After registering Bashlib, an `id` and `secret` value will be shown. These values need 167 | to be provided to this command to be able to automatically create authenticated sessions 168 | without needing interactive login. 169 | 170 | #### options 171 | ``` 172 | Options: 173 | -w, --webid User WebID 174 | -i, --id application registration id 175 | -s, --secret application registration secret 176 | -v, --verbose Log actions 177 | -h, --help display help for command 178 | ``` 179 | The `--webid` option is the WebID for which the token is created. 180 |
181 | The `--id` option is the `id` value retrieved from the registration flow described above. 182 |
183 | The `--secret` option is the `secret` value retrieved from the registration flow described above. 184 |
185 | the `--verbose` option outputs operation logs. 186 | 187 | #### examples 188 | Open interactive dialog to create token 189 | ``` 190 | sld auth create-token-ess 191 | ``` -------------------------------------------------------------------------------- /documentation/markdown/documentation/overview.md: -------------------------------------------------------------------------------- 1 | # Documentation Overview 2 | The Bashlib command line interface provides straightforward commands to interact with your Solid pod. 3 | It can be useful both for personal use, setting up small workflows or quick demonstrations of proof of concepts. 4 | The authentication options enable quick switching between WebIDs and Solid Pods. 5 |
6 | As not everything is optimized for performance, operations that target many resources or few but large resources 7 | may not be performant enough for some use-cases, as the internal resource management does not make use of streaming. 8 | 9 |
10 | To login and manage authentication info for Bashlib, navigate to the authentication management section. 11 |
12 | For an overview of the available commands, their options and code example, navigate to the available commands section. 13 |
14 | To view and manage access to resources, navigate to the access management section. 15 |
16 | To see how to use aliases to speed up your flow, navigate to the using aliases section. 17 | 18 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/setup.md: -------------------------------------------------------------------------------- 1 | # Setting up Bashlib 2 | 3 | 4 | ## Requirements 5 | 6 | - Node >= 16.0.0 7 | 8 | ## Setup 9 | **Using github** 10 | ``` 11 | git clone git@github.com:SolidLabResearch/Bashlib.git 12 | cd Bashlib 13 | npm install 14 | npm run build 15 | ``` 16 | After the install, add an alias to your `.bashrc` for convenience: 17 | ``` 18 | alias sld="node /path/to/folder/.../bin/solid.js" 19 | ``` 20 | 21 | **Using NPX** 22 | ``` 23 | npx solid-bashlib 24 | ``` 25 | This will automatically install any dependencies. 26 | You can add an alias to your `.bashrc` for convenience: 27 | ``` 28 | alias sld="npx solid-bashlib" 29 | ``` 30 | 31 | **Note that while more straightforward, using NPX incurs a performance penalty of up to 1 second! 32 | Consider installing the tool via Github to speed things up!** 33 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/typescript/authentication.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidLabResearch/Bashlib/80de25cbb4b3ed057f95e25bc057f1be9b00cef3/documentation/markdown/documentation/typescript/authentication.md -------------------------------------------------------------------------------- /documentation/markdown/documentation/typescript/css-specific.md: -------------------------------------------------------------------------------- 1 | # css specific typescript 2 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/typescript/example-requests.md: -------------------------------------------------------------------------------- 1 | # Example requests ts 2 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/typescript/metadata.md: -------------------------------------------------------------------------------- 1 | # Handling metadata 2 | -------------------------------------------------------------------------------- /documentation/markdown/documentation/typescript/overview.md: -------------------------------------------------------------------------------- 1 | # Overview Usage for Typescript 2 | -------------------------------------------------------------------------------- /documentation/mkdocs.yml: -------------------------------------------------------------------------------- 1 | docs_dir: markdown 2 | 3 | theme: 4 | name: "material" 5 | custom_dir: overrides 6 | icon: 7 | repo: fontawesome/brands/github 8 | palette: 9 | - media: "(prefers-color-scheme: light)" 10 | scheme: default 11 | toggle: 12 | icon: material/weather-night 13 | name: Switch to dark mode 14 | primary: deep purple 15 | accent: deep orange 16 | 17 | # Palette toggle for dark mode 18 | - media: "(prefers-color-scheme: dark)" 19 | scheme: slate 20 | toggle: 21 | icon: material/weather-sunny 22 | name: Switch to light mode 23 | primary: deep purple 24 | accent: deep orange 25 | features: 26 | - navigation.instant 27 | - navigation.tabs 28 | - navigation.top 29 | - navigation.indexes 30 | 31 | site_name: "Bashlib" 32 | site_url: https://SolidLabResearch.github.io/Bashlib 33 | 34 | repo_url: https://github.com/SolidLabResearch/Bashlib 35 | repo_name: Bashlib 36 | edit_uri: "" 37 | 38 | plugins: 39 | - search 40 | 41 | markdown_extensions: 42 | - admonition 43 | - def_list 44 | - footnotes 45 | - meta 46 | - tables 47 | - toc: 48 | permalink: true 49 | - pymdownx.betterem: 50 | smart_enable: all 51 | - pymdownx.caret 52 | - pymdownx.tilde 53 | - pymdownx.details 54 | - pymdownx.highlight 55 | - pymdownx.superfences 56 | - pymdownx.smartsymbols 57 | - pymdownx.superfences: 58 | custom_fences: 59 | # need to fork the theme to make changes https://github.com/squidfunk/mkdocs-material/issues/3665#issuecomment-1060019924 60 | - name: mermaid 61 | class: mermaid 62 | format: !!python/name:pymdownx.superfences.fence_code_format 63 | 64 | extra: 65 | version: 66 | provider: mike 67 | social: 68 | - icon: fontawesome/brands/github 69 | link: https://github.com/SolidLabResearch/Bashlib 70 | - icon: fontawesome/brands/npm 71 | link: https://www.npmjs.com/package/solid-bashlib 72 | 73 | nav: 74 | - Welcome: 75 | - README.md 76 | - Tutorial: tutorial.md 77 | - Documentation: 78 | - Overview: documentation/overview.md 79 | - Setup: documentation/setup.md 80 | - Manage Authentication: documentation/cli/authentication.md 81 | - Manage Resource Access: documentation/cli/access.md 82 | - Available Commands: documentation/cli/commands.md 83 | - Using Aliases: documentation/cli/aliases.md 84 | # - Typescript: 85 | # - Overview: usage/typescript/overview.md 86 | # - Authentication: usage/typescript/authentication.md 87 | # - Example requests: usage/typescript/example-requests.md 88 | # - Metadata: usage/typescript/metadata.md 89 | # - CSS specific features: usage/typescript/css-specific.md 90 | # - Contributing: 91 | # - Pull requests: contributing/making-changes.md 92 | # - API: ./typedocs/" target="_blank_blank 93 | # To write documentation locally, execute the next line and browse to http://localhost:8000 94 | # npm run mkdocs 95 | -------------------------------------------------------------------------------- /documentation/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block outdated %} 4 | You're not viewing the latest version. 5 | 6 | Click here to go to latest. 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /documentation/typedoc.css: -------------------------------------------------------------------------------- 1 | .tsd-page-toolbar, 2 | .tsd-page-title { 3 | background-color: #7E56C2; 4 | } 5 | -------------------------------------------------------------------------------- /node_trace.1.log: -------------------------------------------------------------------------------- 1 | {"traceEvents":[{"pid":322778,"tid":322778,"ts":60473235924,"tts":38395,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x2","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473238842,"tts":41292,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x3","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473238872,"tts":41322,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x4","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473240035,"tts":42486,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x5","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473241172,"tts":43616,"ph":"b","cat":"node,node.async_hooks","name":"TTYWRAP","dur":0,"tdur":0,"id":"0x6","args":{"data":{"executionAsyncId":1,"triggerAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473241675,"tts":44119,"ph":"b","cat":"node,node.async_hooks","name":"SIGNALWRAP","dur":0,"tdur":0,"id":"0x7","args":{"data":{"executionAsyncId":1,"triggerAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473241796,"tts":44240,"ph":"b","cat":"node,node.async_hooks","name":"TTYWRAP","dur":0,"tdur":0,"id":"0x8","args":{"data":{"executionAsyncId":1,"triggerAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473241938,"tts":44382,"ph":"b","cat":"node,node.async_hooks","name":"TTYWRAP","dur":0,"tdur":0,"id":"0x9","args":{"data":{"executionAsyncId":1,"triggerAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473242116,"tts":44560,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0xa","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473259846,"tts":62254,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0xb","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473260297,"tts":62704,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0xc","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473267364,"tts":69667,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0xd","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473272155,"tts":74450,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0xe","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473276539,"tts":78826,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0xf","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473276573,"tts":78858,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x10","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473278811,"tts":81074,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x11","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473283528,"tts":85783,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x12","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473284070,"tts":86304,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x13","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473290258,"tts":92459,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x14","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473304176,"tts":105909,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x15","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473308799,"tts":110525,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x16","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473308832,"tts":110557,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x17","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473312541,"tts":114262,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x18","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473319963,"tts":121670,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x19","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473325460,"tts":126889,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x1a","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473346413,"tts":147587,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x1b","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473356093,"tts":157245,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x1c","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473401893,"tts":202616,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x1d","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473469343,"tts":269026,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x1e","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473470437,"tts":270119,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x1f","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473663344,"tts":460903,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x20","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473663549,"tts":461107,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x21","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473664947,"tts":462497,"ph":"b","cat":"node,node.async_hooks","name":"TickObject","dur":0,"tdur":0,"id":"0x22","args":{"data":{"triggerAsyncId":1,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473665090,"tts":462640,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x23","args":{"data":{"triggerAsyncId":33,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473665098,"tts":462647,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE","dur":0,"tdur":0,"id":"0x24","args":{"data":{"triggerAsyncId":32,"executionAsyncId":1}}},{"pid":322778,"tid":322778,"ts":60473665271,"tts":462821,"ph":"b","cat":"node,node.async_hooks","name":"TickObject_CALLBACK","dur":0,"tdur":0,"id":"0x22","args":{}},{"pid":322778,"tid":322778,"ts":60473665542,"tts":463092,"ph":"e","cat":"node,node.async_hooks","name":"TickObject_CALLBACK","dur":0,"tdur":0,"id":"0x22","args":{}},{"pid":322778,"tid":322778,"ts":60473665591,"tts":463141,"ph":"b","cat":"node,node.async_hooks","name":"PROMISE_CALLBACK","dur":0,"tdur":0,"id":"0x23","args":{}},{"pid":322778,"tid":322778,"ts":60473204937,"tts":8249,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":322778,"tid":322778,"ts":60473204941,"tts":8255,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"20.11.0"}},{"pid":322778,"tid":322778,"ts":60473204943,"tts":8256,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":322778,"tid":322778,"ts":60473204987,"tts":8301,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"node":"20.11.0","v8":"11.3.244.8-node.17","uv":"1.46.0","zlib":"1.2.13.1-motley-5daffc7","brotli":"1.0.9","ares":"1.20.1","modules":"115","nghttp2":"1.58.0","napi":"9","llhttp":"8.1.1","uvwasi":"0.0.19","acorn":"8.11.2","simdutf":"4.0.4","ada":"2.7.4","undici":"5.27.2","cjs_module_lexer":"1.2.2","base64":"0.5.1","openssl":"3.0.12+quic","cldr":"43.1","icu":"73.2","tz":"2023c","unicode":"15.0","ngtcp2":"0.8.1","nghttp3":"0.7.0"},"arch":"x64","platform":"linux","release":{"name":"node","lts":"Iron"}}}},{"pid":322778,"tid":322781,"ts":60473205198,"tts":136,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"WorkerThreadsTaskRunner::DelayedTaskScheduler"}},{"pid":322778,"tid":322783,"ts":60473205633,"tts":75,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}},{"pid":322778,"tid":322785,"ts":60473205734,"tts":74,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}},{"pid":322778,"tid":322784,"ts":60473205734,"tts":131,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}},{"pid":322778,"tid":322786,"ts":60473205802,"tts":120,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}},{"pid":322778,"tid":322778,"ts":60473204937,"tts":8249,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":322778,"tid":322778,"ts":60473204941,"tts":8255,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"20.11.0"}},{"pid":322778,"tid":322778,"ts":60473204943,"tts":8256,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":322778,"tid":322778,"ts":60473204987,"tts":8301,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"node":"20.11.0","v8":"11.3.244.8-node.17","uv":"1.46.0","zlib":"1.2.13.1-motley-5daffc7","brotli":"1.0.9","ares":"1.20.1","modules":"115","nghttp2":"1.58.0","napi":"9","llhttp":"8.1.1","uvwasi":"0.0.19","acorn":"8.11.2","simdutf":"4.0.4","ada":"2.7.4","undici":"5.27.2","cjs_module_lexer":"1.2.2","base64":"0.5.1","openssl":"3.0.12+quic","cldr":"43.1","icu":"73.2","tz":"2023c","unicode":"15.0","ngtcp2":"0.8.1","nghttp3":"0.7.0"},"arch":"x64","platform":"linux","release":{"name":"node","lts":"Iron"}}}},{"pid":322778,"tid":322781,"ts":60473205198,"tts":136,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"WorkerThreadsTaskRunner::DelayedTaskScheduler"}},{"pid":322778,"tid":322783,"ts":60473205633,"tts":75,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}},{"pid":322778,"tid":322785,"ts":60473205734,"tts":74,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}},{"pid":322778,"tid":322784,"ts":60473205734,"tts":131,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}},{"pid":322778,"tid":322786,"ts":60473205802,"tts":120,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"PlatformWorkerThread"}}]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-bashlib", 3 | "version": "0.6.5", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "sld": "bin/solid.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "build": "rm -rf dist/ && tsc", 12 | "bundle": "npx esbuild bin/solid.js --bundle --platform=node --outfile=bin/bundled.js", 13 | "prepare": "npm run build; npm run bundle", 14 | "release": "release-it", 15 | "mkdocs": "pip install mkdocs mkdocs-material && mkdocs serve -f documentation/mkdocs.yml", 16 | "typedocs": "typedoc --customCss ./documentation/typedoc.css", 17 | "typedocs:dev": "typedoc --customCss ./documentation/typedoc.css; live-server docs/", 18 | "updatesite": "npm run typedocs; mkdocs gh-deploy -f documentation/mkdocs.yml" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@comunica/query-sparql": "^2.1.0", 24 | "@inrupt/solid-client": "1.26.0", 25 | "@inrupt/solid-client-authn-node": "^1.14.0", 26 | "chalk": "^4.1.2", 27 | "cli-columns": "^4.0.0", 28 | "cli-select": "^1.1.2", 29 | "cli-table": "^0.3.11", 30 | "commander": "^13.0.0", 31 | "cross-fetch": "^3.1.5", 32 | "express": "^4.17.3", 33 | "form-urlencoded": "^6.0.6", 34 | "http-link-header": "^1.0.4", 35 | "inquirer": "^8.2.4", 36 | "jose": "^4.7.0", 37 | "jwt-decode": "^3.1.2", 38 | "md5": "^2.3.0", 39 | "mime-types": "^2.1.35", 40 | "open": "^8.4.0", 41 | "set-cookie-parser": "^2.4.8" 42 | }, 43 | "devDependencies": { 44 | "@types/express": "^5.0.1", 45 | "@types/inquirer": "^8.2.1", 46 | "@types/mime-types": "^2.1.4", 47 | "live-server": "^1.2.2", 48 | "release-it": "^15.5.0", 49 | "typedoc": "^0.23.27", 50 | "typescript": "^4.9.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/authentication/AuthenticationInteractive.ts: -------------------------------------------------------------------------------- 1 | import { getOIDCConfig, getSessionInfoFromStorage, StorageHandler, OIDCConfig, readSessionTokenInfo, storeSessionTokenInfo, decodeIdToken, writeErrorString } from '../utils/authenticationUtils'; 2 | import { generateDpopKeyPair, KeyPair, createDpopHeader, buildAuthenticatedFetch } from '@inrupt/solid-client-authn-core'; 3 | import formurlencoded from 'form-urlencoded'; 4 | import { Session } from '@inrupt/solid-client-authn-node'; 5 | import open from 'open'; 6 | import { removeConfigSession, getConfigCurrentSession, getConfigCurrentWebID, ISessionEntry, setConfigCurrentWebID, setConfigSession } from '../utils/configoptions'; 7 | import chalk from 'chalk'; 8 | import { getUserIdp } from './authenticate'; 9 | import BashlibError from '../utils/errors/BashlibError'; 10 | import { BashlibErrorMessage } from '../utils/errors/BashlibError'; 11 | import crossfetch from 'cross-fetch'; 12 | 13 | import express from 'express'; 14 | import { Logger } from '../logger'; 15 | 16 | export interface SessionInfo { 17 | fetch: typeof fetch 18 | webId?: string 19 | } 20 | 21 | export interface IInteractiveAuthOptions { 22 | idp?: string, 23 | sessionInfoStorageLocation?: string, // Storage location of session information to reuse in subsequent runs of the application. 24 | port?: number, // Used for redirect url of Solid login sequence 25 | verbose?: boolean, 26 | logger?: Logger, 27 | } 28 | 29 | export const DEFAULTPORT = 3435 30 | export const APPNAME = "Solid-cli" 31 | 32 | export default async function authenticateInteractive(options: IInteractiveAuthOptions) : Promise { 33 | 34 | let appName = APPNAME 35 | let port = options.port || DEFAULTPORT 36 | 37 | let currentSession = getConfigCurrentSession(); 38 | try { 39 | if (currentSession) { 40 | let sessionInfo = await readSessionTokenInfo(); 41 | if (options.idp && (!sessionInfo.idp || sessionInfo.idp !== options.idp)) 42 | throw new BashlibError(BashlibErrorMessage.cannotRestoreSession) 43 | if (sessionInfo) { 44 | var tokenTimeLeftInSeconds = (sessionInfo.expirationDate.getTime() - new Date().getTime()) / 1000; 45 | if (tokenTimeLeftInSeconds > 60) { 46 | // Only reuse previous session tokens if we have enough time to work with, else continue to create a new access token. 47 | let fetch = await buildAuthenticatedFetch(crossfetch, sessionInfo.accessToken, { dpopKey: sessionInfo.dpopKey }); 48 | let webId = sessionInfo.webId; 49 | // fetch = await wrapFetchRefresh(fetch, sessionInfo.expirationDate, webId as string, options.idp as string, appName, port) as any; 50 | return { fetch, webId } 51 | } else { 52 | // remove timed out session 53 | let webId = getConfigCurrentWebID(); 54 | if (webId) { removeConfigSession(webId) } 55 | } 56 | } 57 | } 58 | } catch (e) { 59 | if (options?.verbose) writeErrorString('Could not load existing session', e, options); 60 | } 61 | 62 | 63 | // Check for available IDP. If not, require one from the user. 64 | if (!options.idp) { 65 | if (currentSession && currentSession.idp) { 66 | console.log(`Continue authenticating with ${chalk.bold(currentSession.idp)} ? [Y/n] `); 67 | options.idp = await new Promise((resolve, reject) => { 68 | process.stdin.setRawMode(true); 69 | process.stdin.resume(); 70 | process.stdin.on('data', (chk) => { 71 | if (chk.toString('utf8') === "n") { 72 | resolve(undefined); 73 | } else { 74 | resolve((currentSession as ISessionEntry).idp); 75 | } 76 | }); 77 | }); 78 | } 79 | } 80 | 81 | if (!options.idp) { options.idp = await getUserIdp() } 82 | if (!options.idp) throw new BashlibError(BashlibErrorMessage.noIDPOption) 83 | 84 | try { 85 | return await createFetchWithNewAccessToken(options.idp, appName, port) 86 | } catch (e) { 87 | if (options?.verbose) writeErrorString('Error creating new session', e, options); 88 | return { fetch: crossfetch } 89 | } 90 | 91 | 92 | } 93 | 94 | /** 95 | * Handle login flow if no existing session can be reused 96 | * @param oidcIssuer 97 | * @param appName 98 | * @param port 99 | * @param storageLocation 100 | * @returns 101 | */ 102 | async function createFetchWithNewAccessToken(oidcIssuer: string, appName: string, port: number) : Promise { 103 | return new Promise( async (resolve, reject) => { 104 | const config = await getOIDCConfig(oidcIssuer); 105 | if (!config) reject(new Error("Could not read oidc config")); 106 | 107 | const app = express(); 108 | const redirectUrl = `http://localhost:${port}/`; 109 | const storage = new StorageHandler(); 110 | 111 | let session : Session = new Session({ 112 | insecureStorage: storage, 113 | secureStorage: storage, 114 | }); 115 | const handleRedirect = (url: string) => { open(url) } 116 | 117 | const server = app.listen(port, async () => { 118 | const loginOptions = { 119 | clientName: appName, 120 | oidcIssuer, 121 | redirectUrl, 122 | tokenType: "DPoP" as "DPoP", // typescript fix 123 | handleRedirect, 124 | }; 125 | try { 126 | await session.login(loginOptions) 127 | } catch (e) { 128 | reject (e) 129 | } 130 | }); 131 | 132 | app.get("/", async (_req: any, res: any) => { 133 | try { 134 | const code = new URL(_req.url, redirectUrl).searchParams.get('code'); 135 | if (!code) throw new BashlibError(BashlibErrorMessage.authFlowError, undefined, 'Server did not return code.') 136 | let { accessToken, expirationDate, dpopKey, webId } = await handleIncomingRedirect(oidcIssuer, redirectUrl, code, storage) 137 | 138 | // Store the session info 139 | storeSessionTokenInfo(accessToken, dpopKey, expirationDate, webId, oidcIssuer) 140 | let fetch = await buildAuthenticatedFetch(crossfetch, accessToken, { dpopKey }); 141 | 142 | // Set the current WebID to the current session 143 | await setConfigCurrentWebID(webId) 144 | server.close(); 145 | resolve({ 146 | fetch, webId 147 | }) 148 | } catch (e) { 149 | reject(new Error('Error authenticating with received token. Please double check your system clock is synced correctly.')) 150 | } 151 | }); 152 | }) 153 | } 154 | 155 | 156 | 157 | /** 158 | * Handles incoming redirect request and returns relevant access token info extracted. 159 | * @param idp 160 | * @param redirectUrl 161 | * @param code 162 | * @param storage 163 | * @returns 164 | */ 165 | export async function handleIncomingRedirect(idp: string, redirectUrl: string, code: string, storage: StorageHandler) { 166 | let config = await getOIDCConfig(idp) 167 | let dpopKey = await generateDpopKeyPair(); 168 | let sessionInfo = await getSessionInfoFromStorage(storage); 169 | if (!sessionInfo || !sessionInfo.clientId || !sessionInfo.clientSecret || !(sessionInfo as any).codeVerifier) 170 | throw new BashlibError(BashlibErrorMessage.cannotCreateSession) 171 | 172 | return await requestAccessToken({ 173 | dpopKey, 174 | code, 175 | codeVerifier: (sessionInfo as any).codeVerifier, 176 | clientId: sessionInfo.clientId, 177 | clientSecret: sessionInfo.clientSecret, 178 | redirectUrl, 179 | config, 180 | }) 181 | 182 | } 183 | 184 | 185 | async function requestAccessToken(p: { 186 | dpopKey: KeyPair, 187 | code: string, 188 | codeVerifier: string, 189 | clientId: string, 190 | clientSecret: string, 191 | redirectUrl: string 192 | config: OIDCConfig 193 | }) : Promise<{ accessToken: string, expirationDate: Date, dpopKey: KeyPair, webId: string }> 194 | { 195 | 196 | 197 | const authString = `${encodeURIComponent(p.clientId)}:${encodeURIComponent(p.clientSecret)}`; 198 | 199 | const response = await fetch(p.config.token_endpoint, { 200 | method: 'POST', 201 | headers: { 202 | authorization: `Basic ${Buffer.from(authString).toString('base64')}`, 203 | 'content-type': 'application/x-www-form-urlencoded', 204 | dpop: await createDpopHeader(p.config.token_endpoint, 'POST', p.dpopKey), 205 | }, 206 | body: formurlencoded({ 207 | grant_type: 'authorization_code', 208 | redirect_uri: p.redirectUrl, 209 | code: p.code, 210 | code_verifier: p.codeVerifier, 211 | client_id: p.clientId, 212 | }), 213 | }); 214 | 215 | let json = await response.json() 216 | if (json.error) { 217 | throw new BashlibError(BashlibErrorMessage.authFlowError, undefined, json.error) 218 | } 219 | 220 | let accessToken = json.access_token; 221 | let tokenExpiratationInSeconds = json.expires_in; 222 | 223 | let currentDate = new Date(); 224 | let expirationDate = new Date(currentDate.getTime() + (1000 * tokenExpiratationInSeconds)) 225 | 226 | let idTokenInfo = decodeIdToken(json.id_token); 227 | let webId = idTokenInfo.webid || idTokenInfo.sub; 228 | if (!idTokenInfo || !webId) 229 | throw new BashlibError(BashlibErrorMessage.authFlowError, undefined, 'Cannot retrieve webid from id token.') 230 | 231 | return { accessToken, expirationDate, dpopKey: p.dpopKey, webId }; 232 | } 233 | -------------------------------------------------------------------------------- /src/authentication/AuthenticationToken.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair } from '@inrupt/solid-client-authn-core'; 2 | import { createDpopHeader, generateDpopKeyPair, buildAuthenticatedFetch } from '@inrupt/solid-client-authn-core'; 3 | import { decodeIdToken, getOIDCConfig, readSessionTokenInfo, storeSessionTokenInfo, writeErrorString } from '../utils/authenticationUtils'; 4 | import { getConfigCurrentToken, getConfigCurrentSession } from '../utils/configoptions'; 5 | import { IClientCredentialsTokenAuthOptions, SessionInfo } from './CreateFetch'; 6 | import BashlibError from '../utils/errors/BashlibError'; 7 | import { BashlibErrorMessage } from '../utils/errors/BashlibError'; 8 | import crossfetch from 'cross-fetch'; 9 | 10 | export async function authenticateWithTokenFromJavascript(token: { id: string, secret: string }, idp: string): Promise { 11 | if (!token) throw new BashlibError(BashlibErrorMessage.noValidToken) 12 | let id = token.id; 13 | let secret = (token as any).secret; 14 | if (!id || !secret) throw new BashlibError(BashlibErrorMessage.noValidToken) 15 | 16 | // A key pair is needed for encryption. 17 | // This function from `solid-client-authn` generates such a pair for you. 18 | const dpopKey = await generateDpopKeyPair(); 19 | 20 | let { accessToken, expirationDate, webId } = await requestAccessToken(id, secret, dpopKey, { idp }); 21 | 22 | let fetch = await buildAuthenticatedFetch(crossfetch, accessToken, { dpopKey }); 23 | 24 | return { fetch, webId } 25 | } 26 | 27 | export async function authenticateToken(options?: IClientCredentialsTokenAuthOptions): Promise { 28 | 29 | let session = getConfigCurrentSession(); 30 | let token = getConfigCurrentToken(); 31 | try { 32 | // If user switches token, cannot reuse older session. 33 | if (session && (!token || token.idp === session.idp)) { 34 | let sessionInfo = await readSessionTokenInfo(); 35 | if (sessionInfo) { 36 | var tokenTimeLeftInSeconds = (sessionInfo.expirationDate.getTime() - new Date().getTime()) / 1000; 37 | if (tokenTimeLeftInSeconds > 60) { 38 | // Only reuse previous session tokens if we have enough time to work with, else continue to create a new access token. 39 | let fetch = await buildAuthenticatedFetch(crossfetch, sessionInfo.accessToken, { dpopKey: sessionInfo.dpopKey }); 40 | let webId = sessionInfo.webId; 41 | return { fetch, webId } 42 | } 43 | } 44 | } 45 | } catch (e) { 46 | if (options?.verbose) writeErrorString('Could not load existing session', e, options); 47 | } 48 | try { 49 | return createFetchWithNewAccessToken(options); 50 | } catch (e) { 51 | if (options?.verbose) writeErrorString('Could not create new session', e, options); 52 | return { fetch: crossfetch } 53 | } 54 | } 55 | 56 | async function createFetchWithNewAccessToken(options?: IClientCredentialsTokenAuthOptions): Promise { 57 | let token = getConfigCurrentToken(); 58 | if (!token) throw new BashlibError(BashlibErrorMessage.noValidToken) 59 | let id = token.id; 60 | let secret = (token as any).secret; 61 | let idp = token.idp; // We stored this cheekily in the token file 62 | if (!id || !secret) throw new BashlibError(BashlibErrorMessage.noValidToken) 63 | 64 | // A key pair is needed for encryption. 65 | // This function from `solid-client-authn` generates such a pair for you. 66 | const dpopKey = await generateDpopKeyPair(); 67 | 68 | if (!options) { options = { idp } } 69 | else if (!options.idp) options.idp = idp; 70 | 71 | let { accessToken, expirationDate, webId } = await requestAccessToken(id, secret, dpopKey, options); 72 | 73 | await storeSessionTokenInfo(accessToken, dpopKey, expirationDate, webId, idp) 74 | let fetch = await buildAuthenticatedFetch(crossfetch, accessToken, { dpopKey }); 75 | 76 | return { fetch, webId } 77 | } 78 | 79 | export async function requestAccessToken(id: string, secret: string, dpopKey: KeyPair, options: IClientCredentialsTokenAuthOptions) { 80 | 81 | // TODO:: other possibility to pass idp here not only store in session obj 82 | let tokenUrl = options.idp 83 | ? (await getOIDCConfig(options.idp)).token_endpoint 84 | : `${options.idp}.oidc/token`; 85 | 86 | // These are the ID and secret generated in the previous step. 87 | // Both the ID and the secret need to be form-encoded. 88 | const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`; 89 | // This URL can be found by looking at the "token_endpoint" field at 90 | // http://localhost:3000/.well-known/openid-configuration 91 | // if your server is hosted at http://localhost:3000/. 92 | 93 | const response = await crossfetch(tokenUrl, { 94 | method: 'POST', 95 | headers: { 96 | // The header needs to be in base64 encoding. 97 | authorization: `Basic ${Buffer.from(authString).toString('base64')}`, 98 | 'content-type': 'application/x-www-form-urlencoded', 99 | dpop: await createDpopHeader(tokenUrl, 'POST', dpopKey), 100 | }, 101 | body: 'grant_type=client_credentials&scope=webid', 102 | }); 103 | if (!response.ok) 104 | throw new BashlibError(BashlibErrorMessage.httpResponseError, tokenUrl, `${response.status} ${response.statusText}`) 105 | 106 | // This is the Access token that will be used to do an authenticated request to the server. 107 | // The JSON also contains an "expires_in" field in seconds, 108 | // which you can use to know when you need request a new Access token. 109 | let json = await response.json(); 110 | let accessToken = json.access_token; 111 | let tokenExpiratationInSeconds = json.expires_in; 112 | 113 | let currentDate = new Date(); 114 | let expirationDate = new Date(currentDate.getTime() + (1000 * tokenExpiratationInSeconds)) 115 | 116 | let idTokenInfo = decodeIdToken(json.access_token); 117 | let webId = idTokenInfo.webid; 118 | if (!idTokenInfo || !webId) 119 | throw new BashlibError(BashlibErrorMessage.authFlowError, undefined, 'Invalid id token received') 120 | 121 | return { accessToken, expirationDate, webId }; 122 | } 123 | -------------------------------------------------------------------------------- /src/authentication/CreateFetch.ts: -------------------------------------------------------------------------------- 1 | import authenticateInteractive from "./AuthenticationInteractive"; 2 | import { authenticateToken } from "./AuthenticationToken"; 3 | import type { Logger } from '../logger'; 4 | 5 | export interface IInteractiveAuthOptions { 6 | idp?: string, 7 | sessionInfoStorageLocation?: string, // Storage location of session information to reuse in subsequent runs of the application. 8 | port?: number, // Used for redirect url of Solid login sequence 9 | verbose?: boolean, 10 | logger?: Logger, 11 | } 12 | 13 | export interface IUserCredentialsAuthOptions { 14 | idp: string, 15 | email: string, 16 | password: string, 17 | port?: number, // Used for redirect url of Solid login sequence 18 | verbose?: boolean, 19 | logger?: Logger, 20 | } 21 | 22 | export interface IClientCredentialsTokenAuthOptions { 23 | idp?: string, // This value is stored with the created client credentials token. 24 | sessionInfoStorageLocation?: string, // Storage location of session information to reuse in subsequent runs of the application. 25 | verbose?: boolean, 26 | logger?: Logger, 27 | } 28 | 29 | export interface ICSSClientCredentialsTokenGenerationOptions { 30 | name: string, 31 | email: string, 32 | password: string, 33 | idp: string, 34 | logger?: Logger, 35 | } 36 | 37 | export interface SessionInfo { 38 | fetch: typeof fetch 39 | webId?: string 40 | } 41 | 42 | class SolidFetchBuilder { 43 | private webId: undefined | string; 44 | private fetch: undefined | typeof fetch; 45 | 46 | buildFromClientCredentialsToken = async (options: IClientCredentialsTokenAuthOptions) => { 47 | const sessionInfo = await authenticateToken(options); 48 | this.webId = sessionInfo.webId; 49 | this.fetch = sessionInfo.fetch; 50 | } 51 | 52 | buildInteractive = async (options: IInteractiveAuthOptions) => { 53 | const sessionInfo = await authenticateInteractive(options); 54 | this.webId = sessionInfo.webId; 55 | this.fetch = sessionInfo.fetch; 56 | } 57 | 58 | getFetch() { return this.fetch } 59 | 60 | getSessionInfo() { return ({ webId: this.webId, fetch: this.fetch }) } 61 | } 62 | 63 | 64 | export default SolidFetchBuilder; -------------------------------------------------------------------------------- /src/authentication/TokenCreationCSS.ts: -------------------------------------------------------------------------------- 1 | import BashlibError from '../utils/errors/BashlibError'; 2 | import { BashlibErrorMessage } from '../utils/errors/BashlibError'; 3 | 4 | const express = require('express') 5 | 6 | export interface ICSSClientCredentialsTokenGenerationOptions { 7 | name: string, 8 | email: string, 9 | password: string, 10 | idp: string, 11 | webId?: string, 12 | } 13 | 14 | export interface IInruptClientCredentialsTokenGenerationOptions { 15 | id: string, 16 | secret: string, 17 | idp: string, 18 | webId?: string, 19 | } 20 | 21 | 22 | export type InruptToken = { 23 | id: string, 24 | secret: string, 25 | idp: string, 26 | } 27 | 28 | export type CSSToken = { 29 | id: string, 30 | secret: string, 31 | controls: any, 32 | name: string, 33 | idp: string, 34 | email: string, 35 | } 36 | 37 | import crossfetch from 'cross-fetch'; 38 | 39 | export async function generateCSSToken(options: ICSSClientCredentialsTokenGenerationOptions) { 40 | return generateCSSTokenVersion7(options) 41 | } 42 | 43 | export async function generateCSSTokenVersion7(options: ICSSClientCredentialsTokenGenerationOptions) { 44 | 45 | if (!options.idp) throw new BashlibError(BashlibErrorMessage.noIDPOption) 46 | if (!options.webId) throw new BashlibError(BashlibErrorMessage.noWebIDOption) 47 | 48 | // For CSS we expect the IDP to be the CSS server. 49 | // No plans to support external IDPs, as they probably do not support this custom token generation anyways? 50 | if (!options.idp.endsWith('/')) options.idp += '/'; 51 | let url = `${options.idp}.account/` 52 | 53 | // All these examples assume the server is running at `http://localhost:3000/`. 54 | // First we request the account API controls to find out where we can log in 55 | const indexResponse = await fetch(url); 56 | const { controls } = await indexResponse.json(); 57 | 58 | // And then we log in to the account API 59 | const response = await crossfetch(controls.password.login, { 60 | method: 'POST', 61 | headers: { 'content-type': 'application/json' }, 62 | body: JSON.stringify({ email: options.email, password: options.password }), 63 | }); 64 | // This authorization value will be used to authenticate in the next step 65 | const { authorization } = await response.json(); 66 | 67 | // Now that we are logged in, we need to request the updated controls from the server. 68 | // These will now have more values than in the previous example. 69 | const indexResponse2 = await fetch(url, { 70 | headers: { authorization: `CSS-Account-Token ${authorization}` } 71 | }); 72 | const controls2 = (await indexResponse2.json()).controls; 73 | // Here we request the server to generate a token on our account 74 | const response2 = await fetch(controls2.account.clientCredentials, { 75 | method: 'POST', 76 | headers: { authorization: `CSS-Account-Token ${authorization}`, 'content-type': 'application/json' }, 77 | // The name field will be used when generating the ID of your token. 78 | // The WebID field determines which WebID you will identify as when using the token. 79 | // Only WebIDs linked to your account can be used. 80 | body: JSON.stringify({ name: options.name, webId: options.webId }), 81 | }); 82 | 83 | // These are the identifier and secret of your token. 84 | // Store the secret somewhere safe as there is no way to request it again from the server! 85 | // The `resource` value can be used to delete the token at a later point in time. 86 | const { id, secret, resource } = await response2.json(); 87 | 88 | const token = { 89 | controls: controls2, 90 | id: id, 91 | secret: secret, 92 | name: options.name, 93 | email: options.email, 94 | idp: options.idp, 95 | } as CSSToken 96 | 97 | return token; 98 | } 99 | 100 | export async function generateCSSTokenVersion6(options: ICSSClientCredentialsTokenGenerationOptions) { 101 | 102 | if (!options.idp) throw new BashlibError(BashlibErrorMessage.noIDPOption) 103 | 104 | if (!options.idp.endsWith('/')) options.idp += '/'; 105 | 106 | // This assumes your server is started under http://localhost:3000/. 107 | // This URL can also be found by checking the controls in JSON responses when interacting with the IDP API, 108 | // as described in the Identity Provider section. 109 | let url = `${options.idp}idp/credentials/` 110 | const response = await crossfetch(url, { 111 | method: 'POST', 112 | headers: { 'content-type': 'application/json' }, 113 | // The email/password fields are those of your account. 114 | // The name field will be used when generating the ID of your token. 115 | body: JSON.stringify({ email: options.email, password: options.password, name: options.name }), 116 | }); 117 | if (!response.ok) 118 | throw new BashlibError(BashlibErrorMessage.httpResponseError, url, `${response.status} ${response.statusText}`) 119 | 120 | // These are the identifier and secret of your token. 121 | // Store the secret somewhere safe as there is no way to request it again from the server! 122 | 123 | const token = await response.json(); 124 | if (token.errorCode) { 125 | throw new BashlibError(BashlibErrorMessage.authFlowError, undefined, `Error retrieving token from server: ${token.name}`) 126 | } 127 | token.name = options.name; 128 | token.email = options.email; 129 | token.idp = options.idp; 130 | 131 | return token as CSSToken; 132 | } 133 | 134 | 135 | export function generateInruptToken(options: IInruptClientCredentialsTokenGenerationOptions): InruptToken { 136 | 137 | return { 138 | id: options.id, 139 | secret: options.secret, 140 | idp: options.idp 141 | } 142 | } -------------------------------------------------------------------------------- /src/authentication/authenticate.ts: -------------------------------------------------------------------------------- 1 | import SolidFetchBuilder from './CreateFetch'; 2 | import { getWebIDIdentityProvider, writeErrorString } from '../utils/util'; 3 | import inquirer from 'inquirer'; 4 | import { getConfigCurrentWebID, getConfigCurrentToken } from '../utils/configoptions'; 5 | import type { Logger } from '../logger'; 6 | import crossfetch from 'cross-fetch'; 7 | 8 | export interface ILoginOptions { 9 | auth?: string, 10 | idp?: string, 11 | identityprovider?: string, 12 | email?: string, 13 | password?: string, 14 | config?: string, 15 | sessionInfoStorageLocation?: string, 16 | verbose?: boolean, 17 | logger?: Logger, 18 | } 19 | 20 | 21 | export default async function authenticate(options: ILoginOptions): Promise<{ fetch: any, webId?: string }> { 22 | let builder = new SolidFetchBuilder; 23 | 24 | options.idp = options.idp || options.identityprovider; // TODO:: make this not necessary :p 25 | 26 | let authType = options.auth 27 | 28 | if (!authType) { 29 | if (getConfigCurrentToken()) authType = "token" 30 | // Try to authenticate interactively 31 | else if (getConfigCurrentWebID() || options.idp) authType = "interactive" 32 | // Ask user for IDP to authenticate interactively 33 | else authType = "request" 34 | } 35 | 36 | if (authType === "request") { 37 | const userWantsToAuthenticate = await queryUserAuthentication(); 38 | if (userWantsToAuthenticate) { 39 | const idp = await getUserIdp() 40 | options.idp = idp 41 | authType = "interactive" 42 | } else { 43 | authType = "none" 44 | } 45 | } 46 | 47 | switch (authType) { 48 | case "none": 49 | return { fetch: crossfetch } 50 | 51 | case "token": 52 | try { 53 | await builder.buildFromClientCredentialsToken(options) 54 | } catch (e) { 55 | if (options.verbose) writeErrorString(`Could not authenticate using client credentials token`, e, options); 56 | throw new Error("Could not authenticate using client credentials token") // TODO:: do this concretely 57 | } 58 | break; 59 | 60 | case "interactive": 61 | try { 62 | await builder.buildInteractive(options); 63 | } catch (e) { 64 | if (options.verbose) writeErrorString(`Could not authenticate interactively`, e, options); 65 | throw new Error("Could not authenticate interactively") // TODO:: do this concretely 66 | } 67 | break; 68 | 69 | default: 70 | throw new Error(`Unknown authentication type: ${authType}`); 71 | } 72 | 73 | let sessionInfo = builder.getSessionInfo(); 74 | if (!sessionInfo || !sessionInfo.fetch) { 75 | console.error('Continuing unauthenticated') 76 | return { fetch: crossfetch } 77 | } else { 78 | return sessionInfo 79 | } 80 | 81 | } 82 | 83 | async function queryUserAuthentication() { 84 | // Ask the user if they want to authenticate. If not, use cross-fetch, else give them a prompt to provide an idp 85 | console.log(`Do you want to authenticate the current request? [Y, n] `); 86 | let userWantsToAuthenticate : boolean = await new Promise((resolve, reject) => { 87 | process.stdin.setRawMode(true); 88 | process.stdin.resume(); 89 | process.stdin.on('data', (chk) => { 90 | if (chk.toString('utf8') === "n") { 91 | resolve(false); 92 | } else { 93 | resolve(true); 94 | } 95 | }); 96 | }); 97 | return userWantsToAuthenticate 98 | } 99 | 100 | 101 | export async function getUserIdp() { 102 | let idp; 103 | let webId = getConfigCurrentWebID() 104 | if (webId) { 105 | idp = await getWebIDIdentityProvider(webId) 106 | } 107 | if (!idp) { 108 | let answers = await inquirer.prompt([{ type: 'input', name: 'webid', message: 'Please provide a WebID to authenticate with.' }]) 109 | idp = await getWebIDIdentityProvider(answers.webid.trim()) 110 | } 111 | if (!idp) throw new Error('No valid WebID value provided.') 112 | return idp && (idp.endsWith('/') ? idp : idp + '/'); 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/commands/solid-command.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from './../logger'; 2 | import fetch from 'cross-fetch'; 3 | 4 | export interface ICommandOptions { 5 | fetch?: typeof globalThis.fetch, 6 | verbose?: boolean, 7 | logger?: Logger, 8 | } 9 | 10 | export interface IPreparedCommandOptions { 11 | fetch: typeof globalThis.fetch, 12 | verbose: boolean, 13 | logger: Logger, 14 | } 15 | 16 | export function setOptionDefaults(options: ICommandOptions) { 17 | if (!options.fetch) options.fetch = fetch 18 | if (!options.verbose) options.verbose = false; 19 | if (!options.logger) options.logger = console; 20 | 21 | return options as IPreparedCommandOptions & T 22 | } -------------------------------------------------------------------------------- /src/commands/solid-edit.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import path from "path" 3 | import copy from "./solid-copy"; 4 | import fs from 'fs'; 5 | import { checkRemoteFileAccess, checkRemoteFileExists } from "../utils/util"; 6 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 7 | import touch from "./solid-touch"; 8 | const mime = require('mime-types'); 9 | 10 | const md5 = require('md5'); 11 | const child_process = require('child_process') 12 | 13 | interface ICommandOptionsEdit extends ICommandOptions { 14 | editor?: string, 15 | touch?: boolean, 16 | contentType?: string, 17 | } 18 | 19 | export default async function edit(url: string, options?: ICommandOptionsEdit) { 20 | let commandOptions = setOptionDefaults(options || {}); 21 | 22 | let exists = await checkRemoteFileExists(url, commandOptions.fetch); 23 | let access = await checkRemoteFileAccess(url, commandOptions.fetch); 24 | 25 | if (exists && access) { 26 | await editRemoteFile(url, commandOptions) 27 | } else if (!exists) { 28 | if (!commandOptions.touch) { 29 | throw new Error('Could not edit non-existing resource. Please use the --touch flag to create a new resource on edit.') 30 | } 31 | await touch(url, commandOptions); 32 | await editRemoteFile(url, commandOptions) 33 | } else { 34 | throw new Error(`No access rights for editing resource at ${url}.`) 35 | } 36 | } 37 | 38 | async function editRemoteFile(url: string, options: ICommandOptionsEdit) { 39 | const systemTmpDir = os.tmpdir() 40 | const solidTmpDir = path.join(systemTmpDir, '.solid/') 41 | 42 | let tmpFilePath: string | undefined; 43 | try { 44 | let copiedData = await copy(url, solidTmpDir, options); 45 | if (!copiedData.destination.files.length) { throw new Error(`Could not retrieve ${url}`) }; 46 | tmpFilePath = copiedData.destination.files[0].absolutePath 47 | let oldMd5 = await fileMD5(tmpFilePath); 48 | let remoteFileUrl = copiedData.source.files[0].absolutePath; 49 | 50 | await new Promise((resolve, reject) => { 51 | var child = child_process.spawn(options.editor, [tmpFilePath], { 52 | stdio: 'inherit' 53 | }); 54 | 55 | child.on('exit', function (e: any, code: any) { 56 | resolve(); 57 | }); 58 | }); 59 | 60 | // Wait for the user to finish editing the 61 | (options.logger || console).log('Press any key to continue'); 62 | await new Promise((resolve, reject) => { 63 | process.stdin.setRawMode(true); 64 | process.stdin.resume(); 65 | process.stdin.on('data', () => resolve()); 66 | }) 67 | 68 | let newMd5 = await fileMD5(tmpFilePath); 69 | 70 | let updateChanges = true; 71 | // Request user update -> required for editors that leave the terminal and continue the program. 72 | if (oldMd5 === newMd5) { 73 | (options.logger || console).log('Update without changes? [y/N] '); 74 | updateChanges = await new Promise((resolve, reject) => { 75 | process.stdin.setRawMode(true); 76 | process.stdin.resume(); 77 | process.stdin.on('data', (chk) => { 78 | if (chk.toString('utf8') === "y") { 79 | resolve(true); 80 | } else { 81 | resolve(false); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | if (updateChanges) { 88 | await copy(tmpFilePath, remoteFileUrl, { ...options, override: true }) 89 | if (options.verbose) (options.logger || console).log('Remote file updated!'); 90 | } 91 | else { 92 | if (options.verbose) (options.logger || console).log('Remote file untouched'); 93 | } 94 | } catch (e) { 95 | throw e 96 | // TODO:: 97 | } finally { 98 | if(tmpFilePath) fs.unlinkSync(tmpFilePath); 99 | if (options.verbose) (options.logger || console).log(`Removing local file file ${tmpFilePath}!`); 100 | } 101 | } 102 | 103 | async function fileMD5(path: string) { 104 | return new Promise( (resolve, reject) => { 105 | fs.readFile(path, (err,buf) => { 106 | if (err) { 107 | reject(err) 108 | } 109 | else { 110 | resolve(md5(buf)); 111 | } 112 | }); 113 | }); 114 | } -------------------------------------------------------------------------------- /src/commands/solid-fetch.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 3 | 4 | interface ICommandOptionsFetch extends ICommandOptions { 5 | header?: string[], 6 | method?: string, 7 | body?: string, 8 | file?: string, // File containing the body 9 | onlyHeaders?: boolean, 10 | } 11 | export default async function authenticatedFetch(url: string, options?: ICommandOptionsFetch) { 12 | let commandOptions = setOptionDefaults(options || {}); 13 | 14 | const fetch = commandOptions.fetch 15 | let processedHeaders : any = {} 16 | for (let header of commandOptions.header || []) { 17 | let split = header.split(':') 18 | processedHeaders[split[0].trim()] = split[1].trim() 19 | } 20 | 21 | if (commandOptions.file && !commandOptions.body){ 22 | commandOptions.body = fs.readFileSync(commandOptions.file, { encoding: "utf-8"}) 23 | } 24 | 25 | const ICommandOptionsFetch = { 26 | method: commandOptions.method, 27 | headers: processedHeaders, 28 | body: commandOptions.body, 29 | // mode: options.mode, 30 | // cache: options.cache, 31 | // credentials: options.credentials, 32 | // redirect: options.redirect, 33 | // referrerPolicy: options.referrerPolicy, 34 | } 35 | 36 | const response = await fetch(url, ICommandOptionsFetch) 37 | if (!response.ok) throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`); 38 | const text = await response.text(); 39 | let methodString = '' 40 | let requestHeaderString = '' 41 | let responseHeaderString = '' 42 | 43 | // Create method string 44 | methodString = `${commandOptions.method || 'GET'} ${url}\n` 45 | 46 | // Create request header string 47 | for (let header of commandOptions.header || []) { 48 | let splitHeader = header.split(':') 49 | requestHeaderString += `> ${splitHeader[0]} ${splitHeader[1]}\n` 50 | } 51 | 52 | // Create response header string 53 | response.headers.forEach((value, key) => { 54 | responseHeaderString += `< ${key} ${value}\n` 55 | }) 56 | 57 | // Log to command line 58 | if (commandOptions.verbose) { 59 | commandOptions.logger.error(methodString); 60 | commandOptions.logger.error(requestHeaderString); 61 | commandOptions.logger.error(responseHeaderString); 62 | } else if (commandOptions.onlyHeaders) { 63 | commandOptions.logger.error(requestHeaderString); 64 | commandOptions.logger.error(responseHeaderString); 65 | } 66 | if (!commandOptions.onlyHeaders) { 67 | commandOptions.logger.log(text.trim()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/commands/solid-find.ts: -------------------------------------------------------------------------------- 1 | import { generateRecursiveListing, FileInfo } from '../utils/util'; 2 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 3 | 4 | export interface ICommandOptionsFind extends ICommandOptions { 5 | all?: boolean, 6 | full?: boolean, 7 | listDirectories?: boolean, 8 | } 9 | 10 | export default async function* find(rootcontainer: string, filename: string, options?: ICommandOptionsFind) { 11 | let commandOptions = setOptionDefaults(options || {}); 12 | 13 | if (!filename || !rootcontainer) return; 14 | for await (let fileInfo of generateRecursiveListing(rootcontainer, commandOptions)) { 15 | const match = processFileNameMatching(filename, fileInfo, commandOptions) 16 | if (match) yield fileInfo 17 | } 18 | } 19 | 20 | function processFileNameMatching(fileName: string, fileInfo: FileInfo, options: ICommandOptionsFind) : boolean { 21 | const regex = new RegExp(fileName) 22 | const name = options.full ? fileInfo.absolutePath : (fileInfo.relativePath || fileInfo.absolutePath) 23 | const match = name.match(regex) 24 | return !!match 25 | 26 | } -------------------------------------------------------------------------------- /src/commands/solid-list.ts: -------------------------------------------------------------------------------- 1 | import { isDirectory, checkHeadersForAclAndMetadata, getResourceInfoFromDataset, getResourceInfoFromHeaders, ResourceInfo, getAclAndMetadata } from '../utils/util'; 2 | import { getContainedResourceUrlAll, getSolidDataset, SolidDataset, WithServerResourceInfo } from '@inrupt/solid-client'; 3 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 4 | 5 | export interface ICommandOptionsList extends ICommandOptions{ 6 | all?: boolean, 7 | full?: boolean, 8 | } 9 | 10 | export default async function list(url: string, options?: ICommandOptionsList) { 11 | let commandOptions = setOptionDefaults(options || {}); 12 | 13 | if (!isDirectory(url)) { 14 | commandOptions.logger.error('List can only be called on containers. Please write containers with their trailing slash.') 15 | throw new Error('List can only be called on containers.'); 16 | } 17 | 18 | // let dataset = await getSolidDataset(url, { fetch: commandOptions.fetch }) 19 | // let containedResources = getContainedResourceUrlAll(dataset) 20 | 21 | let dataset: SolidDataset & WithServerResourceInfo; 22 | let containedResources: string[]; 23 | 24 | try { 25 | dataset = await getSolidDataset(url, { fetch: commandOptions.fetch }) 26 | containedResources = getContainedResourceUrlAll(dataset) 27 | } catch (e) { 28 | throw new Error(`Resource at ${url} does not exist or unauthorized to access resource.`) 29 | } 30 | 31 | let resourceInfos : ResourceInfo[] = [] 32 | 33 | // Test original directory for acl file 34 | if (commandOptions.all) { 35 | let headerInfo = await checkHeadersForAclAndMetadata(url, commandOptions.fetch) 36 | if (headerInfo.acl) { 37 | const resourceInfo = await getResourceInfoFromHeaders(headerInfo.acl, url, commandOptions.fetch) 38 | if(resourceInfo) resourceInfos.push(resourceInfo) 39 | } 40 | if (headerInfo.meta) { 41 | const resourceInfo = await getResourceInfoFromHeaders(headerInfo.meta, url, commandOptions.fetch) 42 | if(resourceInfo) resourceInfos.push(resourceInfo) 43 | } 44 | } 45 | 46 | const promiseList: Promise[] = []; 47 | for (let containedResourceUrl of containedResources) { 48 | promiseList.push(new Promise((resolve, reject) => { 49 | let resourceInfo = getResourceInfoFromDataset(dataset, containedResourceUrl, url); 50 | if (resourceInfo && !resourceInfo.isDir && commandOptions.all) { // We only want to show acl files in the current dir. Aka the ones of the current dir + the ones of contained files 51 | getAclAndMetadata(containedResourceUrl, url, commandOptions.fetch) 52 | .then((headerResources) => { 53 | if (headerResources.acl) resourceInfo.acl = headerResources.acl 54 | if (headerResources.meta) resourceInfo.metadata = headerResources.meta 55 | resolve(resourceInfo); 56 | }) 57 | } else { 58 | resolve(resourceInfo); 59 | } 60 | })) 61 | } 62 | 63 | 64 | const metadataResourceInfoList = (await Promise.all(promiseList)).filter((e) => !!e) 65 | resourceInfos = resourceInfos.concat(metadataResourceInfoList) 66 | metadataResourceInfoList.forEach( (resourceInfo: ResourceInfo) => { 67 | if(resourceInfo.acl) resourceInfos.push(resourceInfo.acl) 68 | if(resourceInfo.metadata) resourceInfos.push(resourceInfo.metadata) 69 | }); 70 | return resourceInfos.filter(resource => resource.url.startsWith(url)) // ugly workaround to .acl and .acp resources not appearing in container 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/solid-mkdir.ts: -------------------------------------------------------------------------------- 1 | import { ResourceInfo } from './../utils/util'; 2 | import { createContainerAt } from '@inrupt/solid-client'; 3 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 4 | 5 | const LDP = "http://www.w3.org/ns/ldp#"; 6 | 7 | export interface ICommandOptionsMakeDirectory extends ICommandOptions { } 8 | 9 | export default async function makeDirectory(url: string, options?: ICommandOptionsMakeDirectory) { 10 | let commandOptions = setOptionDefaults(options || {}); 11 | try { 12 | let container = await createContainerAt(url, { fetch: commandOptions.fetch }); 13 | let info: ResourceInfo = { 14 | url, 15 | isDir: true, 16 | modified: new Date(), 17 | types: [LDP+"Container", LDP+"BasicContainer", LDP+"Resource"], 18 | } 19 | if (commandOptions.verbose) commandOptions.logger.log(`Container successfully created at ${url}`); 20 | return info 21 | } catch (e) { 22 | throw new Error("Target container may exist already") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/solid-move.ts: -------------------------------------------------------------------------------- 1 | import copy from './solid-copy'; 2 | import { isDirectory, isDirectoryContents, isRemote } from '../utils/util'; 3 | import remove from './solid-remove'; 4 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 5 | 6 | export interface ICommandOptionsMove extends ICommandOptions { 7 | all?: boolean, 8 | neverOverride?: boolean, 9 | override?: boolean, 10 | verbose?: boolean, 11 | } 12 | export default async function move(source: string, destination: string, options?: ICommandOptionsMove) { 13 | let commandOptions = setOptionDefaults(options || {}); 14 | 15 | let source_is_dir = isDirectory(source) 16 | let dest_is_dir = isDirectory(destination) 17 | if (source_is_dir && !dest_is_dir) { 18 | commandOptions.logger.error('Cannot move directory to a file') 19 | return; 20 | } 21 | 22 | if (!source_is_dir && dest_is_dir) { 23 | // Define the file to where the resource will be sent 24 | const fileName = source.split('/').slice(-1)[0] 25 | destination = destination + fileName 26 | } 27 | 28 | // Copy from source to destination 29 | await copy(source, destination, commandOptions) 30 | 31 | // Remove source recursively 32 | if (isRemote(source)) { 33 | if (isDirectoryContents(source)) { 34 | const sourceDir = source.substring(0, source.length - 1) 35 | await remove(sourceDir, { recursive: true, saveRoot: true, ...commandOptions }); 36 | } else { 37 | await remove(source, { recursive: true, ...commandOptions }); 38 | } 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /src/commands/solid-perms.ts: -------------------------------------------------------------------------------- 1 | import { getResourceInfo, universalAccess } from "@inrupt/solid-client"; 2 | 3 | import { 4 | AccessModes, 5 | } from '@inrupt/solid-client'; 6 | import { writeErrorString } from '../utils/util'; 7 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 8 | 9 | export interface ICommandOptionsPermissions extends ICommandOptions { } 10 | 11 | export type Record = { 12 | [P in K]: T; 13 | }; 14 | 15 | export type UniversalAccess = { 16 | read: boolean; 17 | append: boolean; 18 | write: boolean; 19 | controlWrite: boolean; 20 | controlRead: boolean; 21 | }; 22 | 23 | 24 | export interface IPermissionListing { 25 | access: { 26 | agent?: null | Record, 27 | public?: null | AccessModes 28 | }, 29 | default?: { 30 | agent?: null | Record, 31 | public?: null | AccessModes 32 | } 33 | resource?: { 34 | agent?: null | Record, 35 | public?: null | AccessModes 36 | } 37 | } 38 | 39 | 40 | export async function listPermissions(resourceUrl: string, options?: ICommandOptionsPermissions) { 41 | let commandOptions = setOptionDefaults(options || {}); 42 | 43 | let permissions : IPermissionListing = { access: {} } 44 | try { 45 | permissions.access.agent = await universalAccess.getAgentAccessAll(resourceUrl, {fetch: commandOptions.fetch}) 46 | permissions.access.public = await universalAccess.getPublicAccess(resourceUrl, {fetch: commandOptions.fetch}) 47 | return permissions 48 | } catch (e) { 49 | if (commandOptions.verbose) writeErrorString(`Could not retrieve universal permissions for ${resourceUrl}`, e, commandOptions) 50 | } 51 | } 52 | 53 | export interface IPermissionOperation { 54 | type: 'agent' | 'public', 55 | id?: string, 56 | read?: boolean, 57 | write?: boolean, 58 | append?: boolean, 59 | control?: boolean, 60 | acl?: boolean, 61 | } 62 | 63 | 64 | // todo: getWebID here 65 | export async function setPermission(resourceUrl: string, operations: IPermissionOperation[], options?: ICommandOptionsPermissions) { 66 | let commandOptions = setOptionDefaults(options || {}); 67 | 68 | for (let operation of operations) { 69 | try { 70 | if (operation.type === 'agent') { 71 | // Update access rights 72 | if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} 73 | let access: UniversalAccess = { read: false, write: false, append: false, controlRead: false, controlWrite: false } 74 | access = updateAccess(access, operation) 75 | // Update local acl for agent with new rights 76 | await universalAccess.setAgentAccess(resourceUrl, operation.id, access, { fetch: commandOptions.fetch }) 77 | } else if (operation.type === 'public') { 78 | // Update access rights 79 | let access: UniversalAccess = { read: false, write: false, append: false, controlRead: false, controlWrite: false } 80 | access = updateAccess(access, operation) 81 | // Update local acl for agent with new rights 82 | await universalAccess.setPublicAccess(resourceUrl, access, { fetch: commandOptions.fetch }) 83 | } else { 84 | if (commandOptions.verbose) writeErrorString("Incorrect operation type", 'Please provide an operation type of agent or public.', commandOptions) 85 | } 86 | commandOptions.logger.log(`Updated permissions for: ${resourceUrl}`) 87 | } catch (e) { 88 | if (commandOptions.verbose) writeErrorString( 89 | `Problem setting permissions for resource ${resourceUrl} operation type`, (e as Error).message, commandOptions 90 | ) 91 | } 92 | } 93 | 94 | } 95 | 96 | function updateAccess(access: UniversalAccess, operation: IPermissionOperation) { 97 | if (operation.read !== undefined) access.read = operation.read 98 | if (operation.write !== undefined) access.write = operation.write 99 | if (operation.append !== undefined) access.append = operation.append 100 | if (operation.control !== undefined) { 101 | access.controlRead = operation.control 102 | access.controlWrite = operation.control 103 | } 104 | return access 105 | } 106 | -------------------------------------------------------------------------------- /src/commands/solid-perms_acl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getAgentAccessAll, 3 | AgentAccess, 4 | Access, 5 | getPublicAccess, 6 | getResourceAcl, 7 | getGroupAccessAll, 8 | getAgentDefaultAccessAll, 9 | getPublicDefaultAccess, 10 | getGroupDefaultAccessAll, 11 | getAgentResourceAccessAll, 12 | getGroupResourceAccessAll, 13 | getPublicResourceAccess, 14 | getResourceInfoWithAcl, 15 | setAgentDefaultAccess, 16 | setGroupDefaultAccess, 17 | setPublicDefaultAccess, 18 | setAgentResourceAccess, 19 | setGroupResourceAccess, 20 | setPublicResourceAccess, 21 | hasAccessibleAcl, 22 | AclDataset, 23 | saveAclFor, 24 | WithAccessibleAcl, 25 | deleteAclFor, 26 | hasResourceAcl, 27 | createAclFromFallbackAcl, 28 | hasFallbackAcl, 29 | createAcl, 30 | } from '@inrupt/solid-client'; 31 | import { writeErrorString } from '../utils/util'; 32 | import type { Logger } from '../logger'; 33 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 34 | 35 | export interface ICommandOptionsPermissions extends ICommandOptions { } 36 | 37 | export type Record = { 38 | [P in K]: T; 39 | }; 40 | 41 | const denyAllAccess = { read: false, write: false, append: false, control: false }; 42 | 43 | 44 | export interface IPermissionListing { 45 | access: { 46 | agent?: null | AgentAccess, 47 | group?: null | Record, 48 | public?: null | Access 49 | }, 50 | default?: { 51 | agent?: null | AgentAccess, 52 | group?: null | Record, 53 | public?: null | Access 54 | } 55 | resource?: { 56 | agent?: null | AgentAccess, 57 | group?: null | Record, 58 | public?: null | Access 59 | } 60 | } 61 | 62 | export async function listPermissions(resourceUrl: string, options?: ICommandOptionsPermissions) { 63 | let commandOptions = setOptionDefaults(options || {}); 64 | 65 | let permissions : IPermissionListing = { access: {} } 66 | try { 67 | const resourceInfo = await getResourceInfoWithAcl(resourceUrl, { fetch: commandOptions.fetch }) 68 | permissions.access.agent = getAgentAccessAll(resourceInfo) 69 | permissions.access.group = getGroupAccessAll(resourceInfo) 70 | permissions.access.public = getPublicAccess(resourceInfo) 71 | 72 | let aclDataset = getResourceAcl(resourceInfo); 73 | 74 | if (aclDataset) { 75 | permissions.default = {}; 76 | permissions.default.agent = getAgentDefaultAccessAll(aclDataset) 77 | permissions.default.group = getGroupDefaultAccessAll(aclDataset) 78 | permissions.default.public = getPublicDefaultAccess(aclDataset) 79 | 80 | permissions.resource = {}; 81 | permissions.resource.agent = getAgentResourceAccessAll(aclDataset) 82 | permissions.resource.group = getGroupResourceAccessAll(aclDataset) 83 | permissions.resource.public = getPublicResourceAccess(aclDataset) 84 | 85 | } 86 | return permissions 87 | } catch (e) { 88 | if (commandOptions.verbose) writeErrorString(`Could not retrieve acl permissions for ${resourceUrl}`, e, commandOptions) 89 | } 90 | } 91 | 92 | export interface IPermissionOperation { 93 | type: 'agent' | 'group' | 'public', 94 | id?: string, 95 | read?: boolean, 96 | write?: boolean, 97 | append?: boolean, 98 | control?: boolean, 99 | default?: boolean, 100 | } 101 | 102 | export async function changePermissions(resourceUrl: string, operations: IPermissionOperation[], options?: ICommandOptionsPermissions) { 103 | let commandOptions = setOptionDefaults(options || {}); 104 | 105 | const resourceInfo = await getResourceInfoWithAcl(resourceUrl, { fetch: commandOptions.fetch }) 106 | let aclDataset : AclDataset | null; 107 | if (await hasResourceAcl(resourceInfo)) { 108 | aclDataset = await getResourceAcl(resourceInfo); 109 | } else { 110 | try { 111 | if (hasFallbackAcl(resourceInfo) && hasAccessibleAcl(resourceInfo)) { 112 | aclDataset = await createAclFromFallbackAcl(resourceInfo) 113 | } else if (hasAccessibleAcl(resourceInfo)) { 114 | aclDataset = await createAcl(resourceInfo) 115 | } else { 116 | throw new Error('No acl found in path to root. This tool requires at least a root acl to be set.'); 117 | } 118 | } catch (e) { 119 | throw new Error(`Could not find fallback ACL file to initialize permissions for ${resourceUrl}: ${(e).message}`) 120 | } 121 | } 122 | if (!aclDataset) { 123 | throw new Error(`You do not have the permissions to edit the ACL file for ${resourceUrl}`) 124 | } 125 | 126 | for (let operation of operations) { 127 | if (operation.type === 'agent') { 128 | // Update access rights 129 | if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} 130 | let access = { read: false, write: false, append: false, control: false } 131 | access = updateAccess(access, operation) 132 | // Update local acl for agent with new rights 133 | if (operation.default) { 134 | // remove non default entry 135 | aclDataset = setAgentResourceAccess(aclDataset, operation.id, access) 136 | // update default entry 137 | aclDataset = setAgentDefaultAccess(aclDataset, operation.id, access) 138 | } else { 139 | // remove default entry 140 | aclDataset = setAgentDefaultAccess(aclDataset, operation.id, denyAllAccess) 141 | // add non default entry 142 | aclDataset = await setAgentResourceAccess(aclDataset, operation.id, access) 143 | } 144 | 145 | 146 | } else if (operation.type === 'group') { 147 | // Update access rights 148 | if (!operation.id) { throw new Error('Please specify group id in the passed operation.')} 149 | let access = { read: false, write: false, append: false, control: false } 150 | access = updateAccess(access, operation) 151 | // Update local acl for group with new rights 152 | if (operation.default) { 153 | // remove non default entry 154 | aclDataset = setGroupResourceAccess(aclDataset, operation.id, access) 155 | // update default entry 156 | aclDataset = setGroupDefaultAccess(aclDataset, operation.id, access) 157 | } else { 158 | // remove default entry 159 | aclDataset = setGroupDefaultAccess(aclDataset, operation.id, denyAllAccess) 160 | // add non default entry 161 | aclDataset = setGroupResourceAccess(aclDataset, operation.id, access) 162 | } 163 | } else if (operation.type === 'public') { 164 | // Update access rights 165 | if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} 166 | let access = { read: false, write: false, append: false, control: false } 167 | access = updateAccess(access, operation) 168 | // Update local acl for agent with new rights 169 | if (operation.default) { 170 | // remove non default entry 171 | aclDataset = setPublicResourceAccess(aclDataset, access) 172 | // update default entry 173 | aclDataset = setPublicDefaultAccess(aclDataset, access) 174 | } else { 175 | // remove default entry 176 | aclDataset = setPublicDefaultAccess(aclDataset, denyAllAccess) 177 | // add non default entry 178 | aclDataset = setPublicResourceAccess(aclDataset, access) 179 | } 180 | } else { 181 | if (commandOptions.verbose) writeErrorString("Incorrect operation type", 'Please provide an operation type of agent, group or public.', commandOptions) 182 | } 183 | } 184 | // Post updated acl to pod 185 | if (aclDataset && await hasAccessibleAcl(resourceInfo)) { 186 | await saveAclFor(resourceInfo as WithAccessibleAcl, aclDataset, {fetch: commandOptions.fetch}) 187 | if (commandOptions.verbose) commandOptions.logger.log(`Updated permissions for: ${resourceUrl}`) 188 | } 189 | } 190 | 191 | export async function deletePermissions(resourceUrl: string, options?: ICommandOptionsPermissions) { 192 | let commandOptions = setOptionDefaults(options || {}); 193 | 194 | let resourceInfo = await getResourceInfoWithAcl(resourceUrl, {fetch: commandOptions.fetch}) 195 | if (hasAccessibleAcl(resourceInfo)) { 196 | await deleteAclFor(resourceInfo, {fetch: commandOptions.fetch}) 197 | if (commandOptions.verbose) commandOptions.logger.log(`Deleted resource at ${resourceUrl}`) 198 | } else { 199 | throw Error(`Resource at ${resourceUrl} does not have an accessible ACL resource`) 200 | } 201 | } 202 | 203 | function updateAccess(access: Access, operation: IPermissionOperation) { 204 | if (operation.read !== undefined) access.read = operation.read 205 | if (operation.write !== undefined) access.write = operation.write 206 | if (operation.append !== undefined) access.append = operation.append 207 | if (operation.control !== undefined) access.control = operation.control 208 | return access 209 | } -------------------------------------------------------------------------------- /src/commands/solid-pod-create.ts: -------------------------------------------------------------------------------- 1 | import { setOptionDefaults, ICommandOptions } from './solid-command'; 2 | 3 | export interface IAccountData { 4 | name: string, 5 | email?: string, 6 | password?: string, 7 | } 8 | 9 | /** 10 | * @description 11 | * Function to initialize an array of data pods on a CSS instance. 12 | */ 13 | export default async function createSolidPods(url: string, accountData: IAccountData[], options?: ICommandOptions) { 14 | let commandOptions = setOptionDefaults(options || {}); 15 | 16 | if (!url) throw new Error('Please pass a value for the CSS pod hosting service'); 17 | 18 | // Uses hardcoded URL. Not sure if this URL can be discovered dynamically? 19 | let pod_server_register_url = url?.endsWith('/') 20 | ? `${url}idp/register/` 21 | : `${url}/idp/register/` 22 | 23 | const responses = [] 24 | for (let account of accountData) { 25 | const settings = { 26 | podName: account.name.toLowerCase(), 27 | email: account.email || `${account.name}@test.edu`, 28 | password: account.password || account.name, 29 | confirmPassword: account.password || account.name, 30 | register: true, 31 | createPod: true, 32 | createWebId: true 33 | } 34 | 35 | const res = await commandOptions.fetch(pod_server_register_url, { 36 | method: 'POST', 37 | headers: { 'content-type': 'application/json', 'Accept': 'application/json' }, 38 | body: JSON.stringify(settings), 39 | }); 40 | // See server response or error text 41 | let jsonResponse = await res.json() 42 | if (jsonResponse.name && jsonResponse.name.includes('Error')) { 43 | commandOptions.logger.error(`${jsonResponse.name} - Creating pod for ${account.name} failed: ${jsonResponse.message}`) 44 | } else { 45 | commandOptions.logger.log(`Pod for ${account.name} created succesfully on ${jsonResponse.webId}`) 46 | responses.push(jsonResponse) 47 | } 48 | } 49 | return responses 50 | } -------------------------------------------------------------------------------- /src/commands/solid-query.ts: -------------------------------------------------------------------------------- 1 | import { QueryEngine } from '@comunica/query-sparql'; 2 | import { isDirectory, writeErrorString, isRDFResource, readRemoteDirectoryRecursively, DirInfo } from '../utils/util'; 3 | import find from './solid-find'; 4 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 5 | 6 | export interface ICommandOptionsQuery extends ICommandOptions{ 7 | all?: boolean, 8 | } 9 | 10 | 11 | export async function queryFederated(containerUrl: string, query: string, options?: ICommandOptionsQuery) { 12 | let commandOptions = setOptionDefaults(options || {}); 13 | 14 | if (!isDirectory(containerUrl)) throw new Error('Executing federated query over single resource!'); 15 | 16 | const directoryInfo: DirInfo = await readRemoteDirectoryRecursively(containerUrl, commandOptions); 17 | const files = directoryInfo.files 18 | 19 | 20 | try { 21 | const rdfResourceURIs: string[] = []; 22 | for (let file of files) { 23 | if (await isRDFResource(file, options?.fetch)) rdfResourceURIs.push(file.absolutePath) 24 | } 25 | const bindings = await queryResource(query, rdfResourceURIs, commandOptions.fetch); 26 | return bindings 27 | } catch (e) { 28 | writeErrorString('Could not evaluate query', e, commandOptions) 29 | } 30 | } 31 | 32 | 33 | export default async function* query(resourceUrl: string, query: string, options?: ICommandOptionsQuery) { 34 | let commandOptions = setOptionDefaults(options || {}); 35 | 36 | if (isDirectory(resourceUrl)) { 37 | for await (let fileInfo of find(resourceUrl, '.', commandOptions)) { 38 | try { 39 | const bindings = await queryResource(query, [fileInfo.absolutePath], commandOptions.fetch) 40 | yield({ fileName: fileInfo.absolutePath, bindings }) 41 | } catch (e) { 42 | if (commandOptions.verbose) writeErrorString('Could not query file', e, commandOptions) 43 | } 44 | } 45 | } else { 46 | try { 47 | const bindings = await queryResource(query, [resourceUrl], commandOptions.fetch) 48 | yield({ fileName: resourceUrl, bindings }) 49 | return 50 | } catch (e) { 51 | writeErrorString('Could not query file', e, commandOptions) 52 | return 53 | } 54 | } 55 | } 56 | 57 | async function queryResource(query: string, sources: any, fetch: typeof globalThis.fetch) { 58 | const queryEngine = new QueryEngine(); 59 | return new Promise(async (resolve, reject) => { 60 | try { 61 | const bindingsStream = await queryEngine.queryBindings(query, { sources, fetch }); 62 | if (!bindingsStream) throw new Error(`Could not query file ${sources}`) 63 | if (!bindingsStream) reject(); 64 | const bindings : any[] = [] 65 | bindingsStream.on('data', (binding: any) => { 66 | bindings.push(binding) 67 | }); 68 | bindingsStream.on('end', () => { resolve(bindings) }); 69 | bindingsStream.on('error', (error: any) => { reject(error) }); 70 | } catch (e) { reject(e) } 71 | }) 72 | 73 | } -------------------------------------------------------------------------------- /src/commands/solid-remove.ts: -------------------------------------------------------------------------------- 1 | import { isDirectory, readRemoteDirectoryRecursively } from '../utils/util'; 2 | import list from '../commands/solid-list'; 3 | import chalk from 'chalk'; 4 | import { deleteContainer, deleteFile } from '@inrupt/solid-client'; 5 | import { ICommandOptions, setOptionDefaults, IPreparedCommandOptions } from './solid-command'; 6 | 7 | export interface ICommandOptionsRemove extends ICommandOptions { 8 | recursive?: boolean, 9 | saveRoot?: boolean, 10 | } 11 | 12 | export default async function remove(url: string, options?: ICommandOptionsRemove) { 13 | let commandOptions = setOptionDefaults(options || {}) 14 | 15 | if (isDirectory(url)) { 16 | const listing = await list(url, { fetch: commandOptions.fetch }) 17 | if(!listing || listing.length === 0) { 18 | // Remove single directory 19 | if (!options || !options.saveRoot) await removeContainer(url, commandOptions) 20 | } else if (!commandOptions.recursive) { 21 | commandOptions.logger.error('Please use the recursive option when removing containers') 22 | return; 23 | } else { 24 | await removeContainerRecursively(url, commandOptions) 25 | } 26 | } else { 27 | await removeFile(url, commandOptions) 28 | } 29 | } 30 | 31 | async function removeFile(url: string, options: ICommandOptionsRemove & IPreparedCommandOptions) { 32 | await deleteFile(url, { fetch: options.fetch }) 33 | if (options.verbose) options.logger.log(`Removed ${url}`) 34 | return; 35 | } 36 | 37 | async function removeContainer(url: string, options: ICommandOptionsRemove & IPreparedCommandOptions) { 38 | await deleteContainer(url, { fetch: options.fetch }) 39 | if (options.verbose) options.logger.log(`Removed ${chalk.blue.bold(url)}`) 40 | return; 41 | 42 | } 43 | 44 | async function removeContainerRecursively(url: string, options: ICommandOptionsRemove & IPreparedCommandOptions) { 45 | let recursiveContainerInfo = await readRemoteDirectoryRecursively(url, options) 46 | let fileInfos = recursiveContainerInfo.files 47 | let containerInfos = recursiveContainerInfo.directories 48 | let containers = containerInfos.map(containerInfo => { 49 | return({ 50 | url: containerInfo.absolutePath, 51 | depth: containerInfo.absolutePath.split('/').length 52 | }) 53 | }) 54 | // The current container info is not returned by the recursive read so we add it manually 55 | containers.push({ 56 | url, 57 | depth: url.split('/').length 58 | }) 59 | let sortedContainerInfo = containers.sort((a, b) => { 60 | if (!a.depth && !b.depth) return 0 61 | if (!a.depth) return 1 62 | if (!b.depth) return -1 63 | if (a.depth === b.depth) return (a.url.localeCompare(b.url)) 64 | return b.depth - a.depth 65 | }) 66 | for (let containerInfo of sortedContainerInfo) { 67 | let containerUrl = containerInfo.url; 68 | if (!containerUrl) { 69 | continue 70 | } 71 | let containedFiles = fileInfos.filter(fileInfo => fileInfo.directory === containerUrl) 72 | for (let file of containedFiles) { 73 | await removeFile(file.absolutePath, options) 74 | } 75 | if (!options || !options.saveRoot) { 76 | await removeContainer(containerUrl, options) 77 | } else if (containerUrl !== url) { 78 | await removeContainer(containerUrl, options) 79 | } 80 | } 81 | } 82 | 83 | function onlyUnique(value: any, index: number, self: any[]) { 84 | return self.indexOf(value) === index; 85 | } -------------------------------------------------------------------------------- /src/commands/solid-shell.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import FetchCommand from '../shell/commands/fetch'; 3 | import ListCommand from '../shell/commands/list'; 4 | import TreeCommand from '../shell/commands/tree'; 5 | import CopyCommand from '../shell/commands/copy'; 6 | import MoveCommand from '../shell/commands/mv'; 7 | import RemoveCommand from '../shell/commands/remove'; 8 | import TouchCommand from '../shell/commands/touch'; 9 | import MkdirCommand from '../shell/commands/mkdir'; 10 | import FindCommand from '../shell/commands/find'; 11 | import QueryCommand from '../shell/commands/query'; 12 | import PermsCommand from '../shell/commands/perms'; 13 | import EditCommand from '../shell/commands/edit'; 14 | import ExitCommand from '../shell/commands/exit'; 15 | import chalk from 'chalk'; 16 | import { addEnvOptions } from '../utils/shellutils'; 17 | import authenticate from '../authentication/authenticate'; 18 | import { checkRemoteFileAccess, getPodRoot } from '../utils/util'; 19 | import ChangedirectoryCommand from '../shell/commands/navigation/cd'; 20 | import { getContainedResourceUrlAll, getSolidDataset, isContainer } from '@inrupt/solid-client'; 21 | 22 | const readline = require('readline'); 23 | 24 | export class SolidShell { 25 | program: Command; 26 | podBaseURI: string | null = null; 27 | workingContainer: string | null = null; 28 | workingContainerEntries: string[] = []; 29 | state: Object; 30 | 31 | constructor(programopts: any) { 32 | this.program = new Command(); 33 | 34 | // pass through the command line options passed to the shell command 35 | for (let key of Object.keys(programopts)) { 36 | this.program.setOptionValue(key, programopts[key]) 37 | } 38 | 39 | fillProgram(this, programopts); 40 | this.state = {}; 41 | } 42 | 43 | async prepareShell(){ 44 | let programOpts = addEnvOptions(this.program.opts() || {}); 45 | 46 | const authenticationInfo = await authenticate(programOpts) 47 | 48 | 49 | // Get current pod working directory 50 | let webId = authenticationInfo.webId 51 | if (!webId) throw new Error('Could not authenticate sesssion.'); 52 | 53 | let podRootURI = await getPodRoot(webId, authenticationInfo.fetch) 54 | this.podBaseURI = podRootURI; 55 | if (podRootURI) await this.changeWorkingContainer(podRootURI) 56 | this.workingContainer = podRootURI; 57 | } 58 | 59 | async runShell(options: any = {}) { 60 | while (true) { 61 | let input = await this.processUserInput(); 62 | let parsedInput = input.match(/(".*?"|[^"\s]+)+(?=\s*|\s*$)/g) 63 | let pargs = ['', ''].concat(parsedInput) 64 | try { 65 | await this.program.parseAsync(pargs); 66 | } catch (_ignored) {} 67 | } 68 | } 69 | 70 | async command_completion(line: string, callback: any) { 71 | 72 | new Promise(async (resolve, reject) => { 73 | let lineArgs = line.split(/\s/).reverse(); 74 | let lastInput = lineArgs[0] || lineArgs[1] 75 | let container = this.workingContainer + lastInput; 76 | container = container.split('/').slice(0, -1).join('/') + '/' 77 | 78 | let workingContainerEntries : string[] = [] 79 | 80 | // Get container entries 81 | try { 82 | const authenticationInfo = await authenticate(this.program.opts()) 83 | let containerDataset = await getSolidDataset(container, { fetch: authenticationInfo.fetch }) 84 | if (!isContainer(containerDataset)) { 85 | throw new Error(`Cannot change container. Target ${container} is not a container.`) 86 | } else if (!checkRemoteFileAccess(container, authenticationInfo.fetch)) { 87 | throw new Error(`Cannot change container. Cannot read target ${container}.`) 88 | } 89 | workingContainerEntries = getContainedResourceUrlAll(containerDataset) || [] 90 | } catch (error) { 91 | reject(error) 92 | } 93 | const completions = workingContainerEntries.map(e => this.podBaseURI ? e.replace(this.podBaseURI, "") : e); 94 | const hits = completions.filter((c) => c.startsWith(lastInput)) 95 | if (hits.length > 1) { 96 | resolve([hits, line]) 97 | } else if (hits.length === 1) { 98 | resolve([hits, container + hits[0]]) 99 | } else { 100 | resolve([completions, line]) 101 | } 102 | }).then((ans: any) => { 103 | callback(null, ans) 104 | }) 105 | } 106 | 107 | async processUserInput(): Promise { 108 | let currentContainerName = this.workingContainer === this.podBaseURI ? "/" : this.workingContainer?.split('/').reverse()[1] 109 | let query = chalk.bold(`[${chalk.green(this.podBaseURI || "Solid Shell")} ${chalk.red(currentContainerName)}]$ `) 110 | const rl = readline.createInterface({ 111 | input: process.stdin, 112 | output: process.stdout, 113 | terminal: true, 114 | completer: this.command_completion.bind(this) 115 | }); 116 | 117 | return new Promise((resolve, reject): Promise => rl.question(query, async (ans: any) => { 118 | rl.close(); 119 | resolve(ans) 120 | })) 121 | } 122 | 123 | /** 124 | * Set the current working container for the shell. 125 | * Fetcha ll contained resources for shell autocompletion purposes. 126 | * @param url 127 | */ 128 | async changeWorkingContainer(url: string) { 129 | //TODO:: Fix authentication and this whole thing. This was made hastily 130 | const authenticationInfo = await authenticate(this.program.opts()) 131 | 132 | let newBaseURI; 133 | let newWorkingContainer = url; 134 | 135 | if (!this.podBaseURI || !url.startsWith(this.podBaseURI)) { 136 | newBaseURI = await getPodRoot(url, authenticationInfo.fetch); 137 | } 138 | 139 | if (newWorkingContainer) { 140 | this.workingContainer = newWorkingContainer || this.workingContainer; 141 | this.podBaseURI = newBaseURI || this.podBaseURI; 142 | } 143 | 144 | try { 145 | let containerDataset = await getSolidDataset(url, { fetch: authenticationInfo.fetch }) 146 | if (!isContainer(containerDataset)) { 147 | throw new Error(`Cannot change container. Target ${url} is not a container.`) 148 | } else if (!checkRemoteFileAccess(url, authenticationInfo.fetch)) { 149 | throw new Error(`Cannot change container. Cannot read target ${url}.`) 150 | } 151 | this.workingContainerEntries = getContainedResourceUrlAll(containerDataset) || [] 152 | } catch (_ignored) { 153 | } 154 | } 155 | } 156 | 157 | export default async function shell(programopts: any) { 158 | let solidShell = new SolidShell(programopts); 159 | await solidShell.prepareShell(); 160 | solidShell.runShell(); 161 | } 162 | 163 | 164 | function fillProgram(shell: SolidShell, programopts: any) { 165 | 166 | let program = shell.program; 167 | 168 | // Make it that the shell does not quit process but just throws exception 169 | program.exitOverride(); 170 | 171 | program 172 | .option('-a, --auth ', 'token | credentials | interactive | none - Authentication type (defaults to "none")') 173 | .option('-i, --idp ', '(auth: any) URL of the Solid Identity Provider') 174 | .option('-e, --email ', '(auth: credentials) Email adres for the user. Default to @test.edu') 175 | .option('-p, --password ', '(auth: credentials) User password. Default to ') 176 | .option('-t, --tokenStorage ', '(auth: token) Location of file storing Client Credentials token. Defaults to ~/.solid/.css-auth-token') 177 | .option('-s, --sessionStorage ', '(auth: token | interactive) Location of file storing session information. Defaults to ~/.solid/.session-info-') 178 | .option('-c, --config ', '(auth: any) Location of config file with authentication info.') 179 | .option('--silent', 'Silence authentication errors') 180 | .option('--port', 'Specify port to be used when redirecting in Solid authentication flow. Defaults to 3435.') 181 | 182 | 183 | program 184 | .name('solid') 185 | .description('Utility toolings for interacting with a Solid server.') 186 | .version('0.2.0') 187 | .enablePositionalOptions() 188 | 189 | program = new FetchCommand(shell, false, true).addCommand(program) 190 | program = new ListCommand(shell, false, true).addCommand(program) 191 | program = new TreeCommand(shell, false, true).addCommand(program) 192 | program = new CopyCommand(shell, false, false).addCommand(program) 193 | program = new MoveCommand(shell, false, false).addCommand(program) 194 | program = new RemoveCommand(shell, false, false).addCommand(program) 195 | program = new TouchCommand(shell, false, false).addCommand(program) 196 | program = new MkdirCommand(shell, false, false).addCommand(program) 197 | program = new FindCommand(shell, false, false).addCommand(program) 198 | program = new QueryCommand(shell, false, false).addCommand(program) 199 | program = new PermsCommand(shell, false, false).addCommand(program) 200 | program = new EditCommand(shell, false, false).addCommand(program) 201 | program = new ExitCommand(shell, false, false).addCommand(program) 202 | program = new ChangedirectoryCommand(shell, false, true).addCommand(program) 203 | 204 | return program; 205 | } 206 | -------------------------------------------------------------------------------- /src/commands/solid-touch.ts: -------------------------------------------------------------------------------- 1 | import { setOptionDefaults, ICommandOptions } from './solid-command'; 2 | import { resourceExists } from "../utils/util"; 3 | const mime = require('mime-types'); 4 | 5 | export interface ICommandOptionsTouch extends ICommandOptions{ 6 | contentType?: string; 7 | } 8 | 9 | export default async function touch(url: string, options?: ICommandOptionsTouch) { 10 | let commandOptions = setOptionDefaults(options || {}); 11 | let fetch = commandOptions.fetch; 12 | 13 | if (url.endsWith('/')) { 14 | throw new Error('Can\'t touch containers only resources') 15 | } 16 | 17 | let urlExists = await resourceExists(url, commandOptions.fetch); 18 | 19 | if (urlExists) { 20 | if (commandOptions.verbose) commandOptions.logger.log(`Remote file already exists`) 21 | } 22 | else { 23 | let path = url.replace(/.*\//,'') // todo: remove this? Might be leftover from shell experiment 24 | 25 | let contentType = options?.contentType 26 | 27 | if (!contentType) { 28 | contentType = path.endsWith('.acl') || path.endsWith('.meta') ? 'text/turtle': path.endsWith('.acp') ? 'application/ld+json': mime.lookup(path) 29 | } 30 | 31 | if (!contentType) { 32 | throw new Error('Could not discover content type for the touched resource. Please add a file extension or add a content type flag.') 33 | } 34 | 35 | let res = await fetch( 36 | url, 37 | { 38 | method: 'PUT', 39 | body: "", 40 | headers: { 41 | 'Content-Type': contentType 42 | } 43 | } 44 | ) 45 | if (res.ok) { 46 | if (commandOptions.verbose) commandOptions.logger.log(`Remote file created`) 47 | } 48 | else { 49 | throw new Error(`HTTP Error Response requesting ${url}: ${res.status} ${res.statusText}`) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/commands/solid-tree.ts: -------------------------------------------------------------------------------- 1 | import find from './solid-find' 2 | import { isDirectory, FileInfo, writeErrorString } from '../utils/util'; 3 | import chalk from 'chalk'; 4 | import { ICommandOptions, setOptionDefaults } from './solid-command'; 5 | 6 | export interface ICommandOptionsTree extends ICommandOptions { 7 | all?: boolean, 8 | } 9 | const WHITESPACE = ' ' 10 | const DASHES = '---' 11 | 12 | /** 13 | * This function is CLI only, as it does not make any sense as a Node.JS export 14 | */ 15 | export default async function tree(url: string, options?: ICommandOptionsTree) { 16 | let commandOptions = setOptionDefaults(options || {}); 17 | 18 | if (!isDirectory(url)) { 19 | throw new Error('Can only call tree with a container as argument.') 20 | } 21 | 22 | commandOptions.logger.log(chalk.bold(url)) 23 | for await (let fileInfo of find(url, '.', { listDirectories: true, ...options } as any )) { 24 | const depth = getDepth(fileInfo) 25 | let outputString = '' 26 | if (!depth) { 27 | if (commandOptions.verbose) writeErrorString('Could not construct a local path for file', fileInfo.absolutePath, options) 28 | } else if (isDirectory(fileInfo.absolutePath)) { 29 | for (let i = 0; i < depth-1; i++) outputString += `|${WHITESPACE}` 30 | outputString += `${chalk.blue.bold(getFileName(fileInfo))}` 31 | 32 | } else { 33 | for (let i = 0; i < depth-1; i++) outputString += `|${WHITESPACE}` 34 | outputString += `|${DASHES} ${getFileName(fileInfo)}` 35 | } 36 | commandOptions.logger.log(outputString) 37 | } 38 | } 39 | 40 | function getDepth(fileInfo: FileInfo) { 41 | if (!fileInfo.relativePath) return; 42 | return fileInfo.relativePath.split('/').length; 43 | } 44 | 45 | function getFileName(fileInfo: FileInfo) { 46 | return isDirectory(fileInfo.absolutePath) 47 | ? fileInfo.absolutePath.split('/').slice(-2)[0] 48 | : fileInfo.absolutePath.split('/').slice(-1)[0] 49 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { SessionInfo } from './authentication/CreateFetch'; 2 | import copy, {ICommandOptionsCopy} from "./commands/solid-copy" 3 | import list, {ICommandOptionsList} from "./commands/solid-list" 4 | import remove, {ICommandOptionsRemove} from "./commands/solid-remove" 5 | import move, {ICommandOptionsMove} from "./commands/solid-move" 6 | import find, {ICommandOptionsFind} from "./commands/solid-find" 7 | import query, {ICommandOptionsQuery} from './commands/solid-query' 8 | import makeDirectory, { ICommandOptionsMakeDirectory } from "./commands/solid-mkdir" 9 | import touch, {ICommandOptionsTouch} from "./commands/solid-touch" 10 | // import shell from "./commands/solid-shell" 11 | import createSolidPods, {IAccountData} from "./commands/solid-pod-create" 12 | import { listPermissions, setPermission, ICommandOptionsPermissions, IPermissionOperation, IPermissionListing, Record } from './commands/solid-perms' 13 | import { changePermissions as setPermissionsAcl, deletePermissions as deletePermissionsAcl } from './commands/solid-perms_acl' 14 | import { authenticateWithTokenFromJavascript } from "./authentication/AuthenticationToken" 15 | import { generateCSSToken, ICSSClientCredentialsTokenGenerationOptions, CSSToken } from "./authentication/TokenCreationCSS" 16 | import { FileInfo, ResourceInfo } from './utils/util'; 17 | 18 | // General Solid functionality 19 | export { copy, list, remove, move, find, query, listPermissions, setPermission, setPermissionsAcl, deletePermissionsAcl, makeDirectory, touch } 20 | 21 | // Authentication Functionality 22 | export { authenticateWithTokenFromJavascript as authenticateToken, generateCSSToken } 23 | 24 | // CSS-specific Functionalitys 25 | export { createSolidPods } 26 | 27 | // Type exports 28 | export type { Logger } from './logger'; 29 | 30 | // Type exports of commands options 31 | export type { ICommandOptionsCopy, ICommandOptionsList, ICommandOptionsRemove, ICommandOptionsMove, ICommandOptionsFind, ICommandOptionsQuery, ICommandOptionsPermissions, ICommandOptionsMakeDirectory, ICommandOptionsTouch } 32 | 33 | // Type exports 34 | export type { IAccountData, ICSSClientCredentialsTokenGenerationOptions, CSSToken, IPermissionOperation, FileInfo, ResourceInfo, SessionInfo, IPermissionListing, Record } -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | log(...msg: string[]): void; 3 | error(...msg: string[]): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/shell/commands/SolidCommand.ts: -------------------------------------------------------------------------------- 1 | import { SolidShell } from '../../commands/solid-shell'; 2 | import { Command } from 'commander'; 3 | export default abstract class SolidCommand { 4 | shell?: SolidShell; 5 | mayExit: boolean; 6 | programopts?: any; 7 | mayUseCurrentContainer: boolean; 8 | 9 | constructor(shell?: SolidShell, mayExit = false, mayUseCurrentContainer = false) { 10 | this.shell = shell; 11 | this.mayExit = mayExit; 12 | this.mayUseCurrentContainer = mayUseCurrentContainer; 13 | } 14 | 15 | abstract addCommand(program: Command): Command; 16 | } -------------------------------------------------------------------------------- /src/shell/commands/copy.ts: -------------------------------------------------------------------------------- 1 | import copy from '../../commands/solid-copy'; 2 | import authenticate from '../../authentication/authenticate'; 3 | import { addEnvOptions, changeUrlPrefixes } from '../../utils/shellutils'; 4 | import { writeErrorString } from '../../utils/util'; 5 | import { Command } from 'commander'; 6 | import SolidCommand from './SolidCommand'; 7 | 8 | export default class CopyCommand extends SolidCommand { 9 | 10 | public addCommand(program: Command) { 11 | this.programopts = program.opts(); 12 | 13 | program 14 | .command('cp') 15 | .description('Copy resources and containers between remote sources or the local file system') 16 | .argument('', 'file or directory to be copied') 17 | .argument('', 'destination to copy file or directory to') 18 | .option('-a, --all', 'Copy .acl files in recursive directory copies') 19 | // .option('-i, --interactive-override', 'Interactive confirmation prompt when overriding existing files') 20 | .option('-o, --override', 'Automatically override existing files') 21 | .option('-n, --never-override', 'Automatically override existing files') 22 | .option('-v, --verbose', 'Log all read and write operations') 23 | .option('-c, --compare-last-modified', 'Skip targets with newer "last-modified" status') 24 | .action(this.executeCommand) 25 | 26 | return program 27 | } 28 | 29 | async executeCommand (src: string, dst: string, options: any) { 30 | let programOpts = addEnvOptions(this.programopts || {}); 31 | const authenticationInfo = await authenticate(programOpts) 32 | let opts = { 33 | fetch: authenticationInfo.fetch, 34 | } 35 | try { 36 | src = await changeUrlPrefixes(authenticationInfo, src) 37 | dst = await changeUrlPrefixes(authenticationInfo, dst) 38 | await copy(src, dst, { ...options, ...opts}) 39 | } catch (e) { 40 | writeErrorString(`Could not copy requested resources from ${src} to ${dst}`, e, options) 41 | if (this.mayExit) process.exit(1) 42 | } 43 | if (this.mayExit) process.exit(0) 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/shell/commands/css-specific/create-pod.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import SolidCommand from '../SolidCommand'; 3 | import inquirer from 'inquirer'; 4 | import createSolidPods from '../../../commands/solid-pod-create' 5 | import { writeErrorString } from '../../../utils/authenticationUtils'; 6 | 7 | export default class CreatePodCommand extends SolidCommand { 8 | 9 | public addCommand(program: Command) { 10 | this.programopts = program.opts(); 11 | 12 | program 13 | .command('create-pod') 14 | .option('-u, --url ', 'URL of the CSS pod hosting service.') 15 | .option('-n, --name ', 'Name for the newly created Solid account.') 16 | .option('-e, --email ', 'Email adres for the user. Default to @test.edu') 17 | .option('-p, --password ', 'User password. Default to ') 18 | .action( async (options) => { 19 | let questions = [] 20 | if (!options.url) questions.push({ type: 'input', name: 'url', message: 'URL of the CSS pod hosting service'}) 21 | if (!options.name) questions.push({ type: 'input', name: 'name', message: 'Pod and user name'}) 22 | if (!options.email) questions.push({ type: 'input', name: 'email', message: 'User email (defaults to @test.edu)'}) 23 | if (!options.password) questions.push({ type: 'password', name: 'password', message: 'User password (defaults to )'}) 24 | if (questions.length) { 25 | let answers = await inquirer.prompt(questions) 26 | options = { ...options, ...answers } 27 | } 28 | 29 | let accountDataArray = [{ 30 | name: options.name, 31 | email: options.email, 32 | password: options.password, 33 | }] 34 | try { 35 | await createSolidPods(options.url, accountDataArray) 36 | } catch (e) { 37 | writeErrorString(`Could not create pod`, e, options) 38 | } 39 | }) 40 | 41 | 42 | return program 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/shell/commands/edit.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import edit from '../../commands/solid-edit'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, changeUrlPrefixes } from '../../utils/shellutils'; 5 | import { isDirectory, writeErrorString } from '../../utils/util'; 6 | import { SolidShell } from '../../commands/solid-shell'; 7 | import SolidCommand from './SolidCommand'; 8 | 9 | 10 | var editor = process.env.EDITOR || 'vi'; 11 | 12 | export default class EditCommand extends SolidCommand { 13 | 14 | public addCommand(program: Command) { 15 | this.programopts = program.opts(); 16 | 17 | program 18 | .command('edit') 19 | .description('Edit remote resources') 20 | .argument('', 'Resource URL') 21 | .option('-e, --editor ', 'Use custom editor') 22 | .option('-t, --touch', 'Create file if not exists') // Should this be default? 23 | .option('-c, --content-type ', 'Content type of the created resource when using --touch') 24 | .option('-v, --verbose', 'Log all operations') // Should this be default? 25 | .action(async (url, options) => { 26 | let programOpts = addEnvOptions(program.opts() || {}); 27 | const authenticationInfo = await authenticate(programOpts) 28 | options.fetch = authenticationInfo.fetch; 29 | options.editor = options.editor || editor 30 | try { 31 | url = await changeUrlPrefixes(authenticationInfo, url) 32 | if (isDirectory(url)) { 33 | console.error('Cannot edit containers, only single files.') 34 | process.exit(1); 35 | } 36 | await edit(url, options) 37 | } catch (e) { 38 | writeErrorString(`Could not edit resource at ${url}`, e, options) 39 | if (this.mayExit) process.exit(1) 40 | } 41 | if (this.mayExit) process.exit(0) 42 | }) 43 | return program 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/shell/commands/exit.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import SolidCommand from './SolidCommand'; 3 | 4 | export default class ExitCommand extends SolidCommand { 5 | 6 | public addCommand(program: Command) { 7 | this.programopts = program.opts(); 8 | 9 | program 10 | .command('exit') 11 | .description('Exit the shell.') 12 | .action(() => process.exit(0)) 13 | 14 | program 15 | .command('quit') 16 | .description('Exit the shell.') 17 | .action(() => process.exit(0)) 18 | 19 | return program 20 | } 21 | } -------------------------------------------------------------------------------- /src/shell/commands/fetch.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import authenticatedFetch from '../../commands/solid-fetch'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, arrayifyHeaders, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 5 | import { writeErrorString } from '../../utils/util'; 6 | import SolidCommand from './SolidCommand'; 7 | 8 | export default class FetchCommand extends SolidCommand { 9 | public addCommand(program: Command) { 10 | this.programopts = program.opts(); 11 | let urlParam = this.mayUseCurrentContainer ? '[url]' : '' 12 | 13 | program 14 | // .command('cat') 15 | // .description('Utility to display files from remote Solid pod.') 16 | // .argument(urlParam, 'file to be displayed') 17 | // .action(async (url: string, options: any) => { 18 | // await this.executeCommand(url, options) 19 | // }) 20 | 21 | program 22 | .command('curl') 23 | .description('Fetch a resource') 24 | .argument(urlParam, 'file to be fetched') 25 | .option('-v, --verbose', 'Write out full response and all headers') 26 | .option('-H, --only-headers', 'Only write out headers') 27 | .option('-m, --method ', 'GET, POST, PUT, DELETE, ...') 28 | .option('-b, --body ', 'The request body') 29 | .option('-F, --file ', 'File containing the request body. Ignored when the --body flag is set.') 30 | .option('-h, --header ', 'The request header. Multiple headers can be added separately. e.g. -h "Accept: application/json" -h "..." ', arrayifyHeaders) 31 | .action(async (url: string, options: any) => { 32 | await this.executeCommand(url, options) 33 | }) 34 | 35 | return program 36 | } 37 | 38 | private async executeCommand(url?: string, options?: any) { 39 | let programOpts = addEnvOptions(this.programopts || {}); 40 | const authenticationInfo = await authenticate(programOpts) 41 | options.fetch = authenticationInfo.fetch 42 | try { 43 | if (!url) url = this.shell?.workingContainer || undefined; 44 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 45 | if (!url) throw new Error('No valid url parameter passed') 46 | url = await changeUrlPrefixes(authenticationInfo, url) 47 | await authenticatedFetch(url, options) 48 | } catch (e) { 49 | writeErrorString(`Could not fetch resource at ${url}`, e, options) 50 | if (this.mayExit) process.exit(1) 51 | } 52 | if (this.mayExit) process.exit(0) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/shell/commands/find.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import authenticate from '../../authentication/authenticate'; 3 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 4 | import { writeErrorString } from '../../utils/util'; 5 | import find from '../../commands/solid-find' 6 | import { SolidShell } from '../../commands/solid-shell'; 7 | import SolidCommand from './SolidCommand'; 8 | 9 | export default class FindCommand extends SolidCommand { 10 | 11 | public addCommand(program: Command) { 12 | this.programopts = program.opts(); 13 | 14 | program 15 | .command('find') 16 | .description('Find resources') 17 | .argument('', 'Container to start the search') 18 | .argument('', 'Filename to match, processed as RegExp(filename)') 19 | .option('-a, --all', 'Match .acl and .meta files') 20 | .option('-f, --full', 'Match full filename.') 21 | .option('-v, --verbose', 'Log all operations') 22 | .action(async (url: string, filename: string, options: any) => { 23 | let programOpts = addEnvOptions(program.opts() || {}); 24 | const authenticationInfo = await authenticate(programOpts) 25 | options.fetch = authenticationInfo.fetch 26 | try { 27 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 28 | url = await changeUrlPrefixes(authenticationInfo, url) 29 | for await (let fileInfo of find(url, filename, options)) { 30 | const name = options.full ? fileInfo.absolutePath : (fileInfo.relativePath || fileInfo.absolutePath) 31 | // Emit results to console 32 | console.log(name) 33 | } 34 | } catch (e) { 35 | writeErrorString(`Could not find match in ${url}`, e, options) 36 | if (this.mayExit) process.exit(1) 37 | } 38 | if (this.mayExit) process.exit(0) 39 | }) 40 | 41 | return program 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shell/commands/list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import list from '../../commands/solid-list'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, changeUrlPrefixes, getResourceInfoRelativePath, normalizeURL, getAndNormalizeURL } from '../../utils/shellutils'; 5 | import { writeErrorString, ResourceInfo } from '../../utils/util'; 6 | import chalk from 'chalk'; 7 | import SolidCommand from './SolidCommand'; 8 | const columns = require('cli-columns'); 9 | 10 | export default class ListCommand extends SolidCommand { 11 | 12 | public addCommand(program: Command) { 13 | this.programopts = program.opts(); 14 | let urlParam = this.mayUseCurrentContainer ? '[url]' : '' 15 | 16 | program 17 | .command('ls') 18 | .description('List files in a container') 19 | .argument(urlParam, 'URL of container to be listed') 20 | .option('-a, --all', 'List all files including acl files') 21 | .option('-f, --full', 'List files with their full uri') 22 | .option('-l, --long', 'List in long format') 23 | .option('-v, --verbose', '') 24 | .action(async (url: string, options: any) => { 25 | await this.executeCommand(url, options) 26 | }) 27 | 28 | return program 29 | } 30 | 31 | private async executeCommand(url?: string, options?: any) { 32 | let programOpts = addEnvOptions(this.programopts || {}); 33 | const authenticationInfo = await authenticate(programOpts) 34 | options.fetch = authenticationInfo.fetch 35 | let listings: ResourceInfo[] = [] 36 | try { 37 | if (this.shell) { 38 | if (!url) url = this.shell?.workingContainer || undefined; 39 | url = getAndNormalizeURL(url, this.shell); 40 | } 41 | if (!url) throw new Error('No valid url parameter passed') 42 | url = await changeUrlPrefixes(authenticationInfo, url) 43 | listings = await list(url, options) 44 | } catch (e) { 45 | writeErrorString(`Could not provide listing for ${url}`, (e as Error).message, options) 46 | if (this.mayExit) process.exit(1) 47 | } 48 | // Output to command line 49 | console.log(formatListing(listings, options)) 50 | if (this.mayExit) process.exit(0) 51 | } 52 | } 53 | 54 | 55 | /** 56 | * 57 | * @param {ResourceInfo[]} listings 58 | * @param {ICommandOptionsList} options 59 | * @returns 60 | */ 61 | function formatListing(listings: any[], options: any) { 62 | if (!options.long) { 63 | // Write short formatted 64 | let values = listings.map((listingInfo) => { 65 | let path = options.full 66 | ? listingInfo.url 67 | : getResourceInfoRelativePath(listingInfo) 68 | 69 | if (listingInfo.isDir) return chalk.blue.bold(path) 70 | else if (path.endsWith('.acl')) return chalk.red(path) 71 | else if (path.endsWith('.acp')) return chalk.red(path) 72 | else if (path.endsWith('.meta')) return chalk.greenBright(path) 73 | else return path 74 | }) 75 | return columns(values) 76 | } else { 77 | // Write long formatted 78 | const fileNameLengths = listings.map(fileInfo => options.full ? fileInfo.url.length : getResourceInfoRelativePath(fileInfo).length) 79 | const fileNameFieldLength = Math.max(...[Math.max(...fileNameLengths.map(x => x || 0)), 8]) 80 | 81 | const aclLengths = listings.map(fileInfo => fileInfo.acl ? (options.full ? fileInfo.acl.url.length : getResourceInfoRelativePath(fileInfo.acl).length) : 0) 82 | const aclFieldLength = Math.max(...[Math.max(...aclLengths.map(x => x || 0)), 3]) 83 | 84 | const metaLengths = listings.map(fileInfo => fileInfo.metadata ? (options.full ? fileInfo.metadata.url.length : getResourceInfoRelativePath(fileInfo.metadata).length) : 0) 85 | const metaFieldLength = Math.max(...[Math.max(...metaLengths.map(x => x || 0)), 4]) 86 | 87 | const mtimeLength = listings.map(listingInfo => listingInfo.mtime ? listingInfo.mtime.toString().length : 0) 88 | const mtimeFieldLength = Math.max(...[Math.max(...mtimeLength), 5]) 89 | 90 | const sizeLengths = listings.map(listingInfo => listingInfo.size ? listingInfo.size.toString().length : 0) 91 | const sizeFieldLength = Math.max(...[Math.max(...sizeLengths), 4]) 92 | 93 | const modifiedLengths = listings.map(listingInfo => listingInfo.modified ? listingInfo.modified.toISOString().length : 0) 94 | const modifiedFieldLength = Math.max(...[Math.max(...modifiedLengths), 8]) 95 | 96 | const titleFilenameString = "filename".padEnd(fileNameFieldLength) 97 | const titleMTimeString = "mtime".padEnd(mtimeFieldLength) 98 | const titleSizeString = "size".padEnd(sizeFieldLength) 99 | const titleModifiedString = "modified".padEnd(modifiedFieldLength) 100 | const titleAclString = "acl".padEnd(aclFieldLength) 101 | const titleMetaString = "meta".padEnd(metaFieldLength) 102 | 103 | // SORT the listings 104 | listings.sort((a, b) => (a.url).localeCompare(b.url)) 105 | 106 | let output = '' 107 | output += `${titleFilenameString} | ${titleMTimeString} | ${titleSizeString} | ${titleModifiedString} | ${titleAclString} | ${titleMetaString}\n` 108 | output += `${'-'.repeat(fileNameFieldLength + mtimeFieldLength + sizeFieldLength + modifiedFieldLength + aclFieldLength + metaFieldLength + 16)}\n` 109 | for (let listingInfo of listings) { 110 | const path = (options.full ? listingInfo.url : getResourceInfoRelativePath(listingInfo)) || '' 111 | 112 | let pathString = ''; 113 | if (listingInfo.isDir) pathString = chalk.blue.bold(path.padEnd(fileNameFieldLength)) 114 | else if (path.endsWith('.acl')) pathString = chalk.red(path.padEnd(fileNameFieldLength)) 115 | else if (path.endsWith('.acp')) pathString = chalk.red(path.padEnd(fileNameFieldLength)) 116 | else if (path.endsWith('.meta')) pathString = chalk.greenBright(path.padEnd(fileNameFieldLength)) 117 | else pathString = path.padEnd(fileNameFieldLength) 118 | 119 | const mtime = (listingInfo.mtime ? listingInfo.mtime.toString() : '').padEnd(mtimeFieldLength) 120 | const size = (listingInfo.size ? listingInfo.size.toString() : '').padEnd(sizeFieldLength) 121 | const modified = (listingInfo.modified ? listingInfo.modified.toISOString() : '').padEnd(modifiedFieldLength) 122 | const aclPath = listingInfo.acl ? (options.full ? listingInfo.acl.url : getResourceInfoRelativePath(listingInfo.acl)) : '' 123 | const acl = aclPath.padEnd(aclFieldLength) 124 | const metaPath = listingInfo.metadata ? (options.full ? listingInfo.metadata.url : getResourceInfoRelativePath(listingInfo.metadata)) : '' 125 | const meta = metaPath.padEnd(metaFieldLength) 126 | output += `${pathString} | ${mtime} | ${size} | ${modified} | ${acl} | ${meta}\n` 127 | } 128 | return(output) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/shell/commands/mkdir.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import authenticate from '../../authentication/authenticate'; 3 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 4 | import { writeErrorString } from '../../utils/util'; 5 | import makeDirectory from '../../commands/solid-mkdir'; 6 | import SolidCommand from './SolidCommand'; 7 | 8 | export default class MkdirCommand extends SolidCommand { 9 | 10 | public addCommand(program: Command) { 11 | this.programopts = program.opts(); 12 | 13 | program 14 | .command('mkdir') 15 | .description('Create a container') 16 | .argument('', 'Container to start the search') 17 | .option('-v, --verbose', 'Log all operations') 18 | .action(async (url, options) => { 19 | let programOpts = addEnvOptions(program.opts() || {}); 20 | const authenticationInfo = await authenticate(programOpts) 21 | options.fetch = authenticationInfo.fetch 22 | try { 23 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 24 | url = await changeUrlPrefixes(authenticationInfo, url) 25 | await makeDirectory(url, options) 26 | } catch (e) { 27 | writeErrorString(`Could not create container at ${url}`, e, options) 28 | if (this.mayExit) process.exit(1) 29 | } 30 | if (this.mayExit) process.exit(0) 31 | }) 32 | 33 | return program 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/shell/commands/mv.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import move from '../../commands/solid-move'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 5 | import { writeErrorString } from '../../utils/util'; 6 | import SolidCommand from './SolidCommand'; 7 | 8 | export default class MkdirCommand extends SolidCommand { 9 | 10 | public addCommand(program: Command) { 11 | this.programopts = program.opts(); 12 | 13 | program 14 | .command('mv') 15 | .description('Move resources and containers between remote sources or the local file system') 16 | .argument('', 'file or directory to be moved') 17 | .argument('', 'destination of the move') 18 | .option('-a, --all', 'Move .acl files when moving directories recursively') 19 | // .option('-i, --interactive-override', 'Interactive confirmation prompt when overriding existing files') 20 | .option('-o, --override', 'Automatically override existing files') 21 | .option('-n, --never-override', 'Automatically override existing files') 22 | .option('-v, --verbose', 'Log all operations') 23 | .action(this.executeCommand) 24 | 25 | return program 26 | } 27 | 28 | async executeCommand (src: string, dst: string, options: any) { 29 | let programOpts = addEnvOptions(this.programopts || {}); 30 | const authenticationInfo = await authenticate(programOpts) 31 | options.fetch = authenticationInfo.fetch 32 | try { 33 | if (this.shell) src = getAndNormalizeURL(src, this.shell); 34 | if (this.shell) dst = getAndNormalizeURL(dst, this.shell); 35 | src = await changeUrlPrefixes(authenticationInfo, src) 36 | dst = await changeUrlPrefixes(authenticationInfo, dst) 37 | await move(src, dst, options) 38 | } catch (e) { 39 | writeErrorString(`Could not move requested resources from ${src} to ${dst}`, e, options) 40 | if (this.mayExit) process.exit(1) 41 | } 42 | if (this.mayExit) process.exit(0) 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/shell/commands/navigation/cd.ts: -------------------------------------------------------------------------------- 1 | import { getResourceInfo } from '@inrupt/solid-client'; 2 | import { isContainer } from '@inrupt/solid-client'; 3 | import authenticate from '../../../authentication/authenticate'; 4 | import { addEnvOptions, normalizeURL, getAndNormalizeURL, changeUrlPrefixes } from '../../../utils/shellutils'; 5 | import { checkRemoteFileAccess, writeErrorString } from '../../../utils/util'; 6 | import { Command } from 'commander'; 7 | import SolidCommand from '../SolidCommand'; 8 | 9 | export default class ChangedirectoryCommand extends SolidCommand { 10 | 11 | public addCommand(program: Command) { 12 | this.programopts = program.opts(); 13 | let urlParam = this.mayUseCurrentContainer ? '[url]' : '' 14 | 15 | program 16 | .command('cd') 17 | .description('Utility to navigate between containers in a Solid pod.') 18 | .argument(urlParam, 'container to navigate to. Can be a relative path or a full URL.') 19 | .action(async (url: string, options: any) => { 20 | await this.executeCommand(url, options) 21 | }) 22 | 23 | return program 24 | } 25 | 26 | private async executeCommand(url?: string, options?: any) { 27 | let programOpts = addEnvOptions(this.programopts || {}); 28 | const authenticationInfo = await authenticate(programOpts) 29 | options.fetch = authenticationInfo.fetch 30 | if (!this.shell) 31 | throw new Error('Cannot access the current shell to update current working container.') 32 | if (!this.shell.podBaseURI) 33 | throw new Error('Cannot discover pod base url.') 34 | 35 | try { 36 | if (!url) url = this.shell?.podBaseURI || undefined; 37 | url = getAndNormalizeURL(url, this.shell); 38 | url = await changeUrlPrefixes(authenticationInfo, url) 39 | if (!url) throw new Error('Could not discover url value or pod root location.') 40 | if (!url.endsWith('/')) url = url + '/' 41 | if (this.shell.podBaseURI.startsWith(url) && this.shell.podBaseURI !== url) 42 | throw new Error('Cannot change directory to this location. Please provide a relative location within the data pod, or provide an absolute URI.') 43 | if (await checkRemoteFileAccess(url, authenticationInfo.fetch) && isContainer(await getResourceInfo(url, {fetch: authenticationInfo.fetch}))) { 44 | await this.shell.changeWorkingContainer(url); 45 | } else { 46 | console.error(`Could not change working container to ${url}: No such container`) 47 | if (this.mayExit) process.exit(1) 48 | } 49 | } catch (e) { 50 | writeErrorString(`Could not change working container to ${url}`, e, options) 51 | if (this.mayExit) process.exit(1) 52 | } 53 | if (this.mayExit) process.exit(0) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/shell/commands/navigation/cs.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidLabResearch/Bashlib/80de25cbb4b3ed057f95e25bc057f1be9b00cef3/src/shell/commands/navigation/cs.ts -------------------------------------------------------------------------------- /src/shell/commands/perms.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import * as acl_perms from '../../commands/solid-perms_acl'; 3 | import { setPermission, listPermissions, IPermissionOperation, IPermissionListing } from '../../commands/solid-perms'; 4 | import authenticate from '../../authentication/authenticate'; 5 | import { addEnvOptions, changeUrlPrefixes } from '../../utils/shellutils'; 6 | import { discoverAccessMechanism, writeErrorString } from '../../utils/util'; 7 | import chalk from 'chalk'; 8 | import SolidCommand from './SolidCommand'; 9 | const Table = require('cli-table'); 10 | 11 | export default class PermsCommand extends SolidCommand { 12 | 13 | public addCommand(program: Command) { 14 | this.programopts = program.opts(); 15 | 16 | 17 | const access = program 18 | .command('access') 19 | .description('Control and edit resource permissions.'); 20 | 21 | 22 | 23 | access 24 | .command('list') 25 | .argument('', 'Resource URL') 26 | .option('-p, --pretty', 'Pretty format') 27 | .option('-v, --verbose', 'Log all operations') 28 | .action(async (url: string, options: any) => { 29 | 30 | let programOpts = addEnvOptions(this.programopts || {}); 31 | const authenticationInfo = await authenticate(programOpts) 32 | options.fetch = authenticationInfo.fetch 33 | url = await changeUrlPrefixes(authenticationInfo, url) 34 | 35 | const { acp, acl } = await discoverAccessMechanism(url, options.fetch) 36 | if ( !acp && !acl ) { 37 | if (options.verbose) writeErrorString(`Could not list permissions for ${url}`, { message: "Could not find attached WAC or ACP management resource." }, options) 38 | return; 39 | } 40 | if (acl) { 41 | try { 42 | const listings = await acl_perms.listPermissions(url, options) 43 | if (listings?.access.agent || listings?.access.public) { 44 | await formatACLPermissionListing(url, listings, options) 45 | return; 46 | } 47 | } catch (e) { 48 | if (options.verbose) writeErrorString('Unable to list permissions using WAC', e, options) 49 | } 50 | } 51 | if (acp) { 52 | try { 53 | const listings = await listPermissions(url, options) 54 | if (listings?.access.agent || listings?.access.public) { 55 | await formatPermissionListing(url, listings, options) 56 | return; 57 | } 58 | } catch (e) { 59 | if (options.verbose) writeErrorString('Unable to list permissions for ACP', e, options) 60 | } 61 | } 62 | 63 | if (this.mayExit) process.exit(0) 64 | }) 65 | 66 | access 67 | .command('set') 68 | .argument('', 'Resource URL') 69 | .argument('[permissions...]', 70 | `Permission format when setting permissions. 71 | Format according to id=[a][c][r][w]. 72 | For public permissions please set id to "p". 73 | For the current authenticated user please set id to "u". 74 | For specific agents, set id to be the agent webid. 75 | `) 76 | .option('--default', 'Set the defined permissions as default (only for pods on a WAC-based solid server)') 77 | .option('--group', 'Process identifier as a group identifier (only for pods on a WAC-based solid server)') 78 | .option('-v, --verbose', 'Log all operations') // Should this be default? 79 | .action( async (url: string, permissions: string[], options: any) => { 80 | 81 | let programOpts = addEnvOptions(this.programopts || {}); 82 | const authenticationInfo = await authenticate(programOpts) 83 | options.fetch = authenticationInfo.fetch 84 | url = await changeUrlPrefixes(authenticationInfo, url) 85 | 86 | try { 87 | // if (this.shell) url = getAndNormalizeURL(url, this.shell); 88 | let parsedPermissions = permissions.map(permission => { 89 | const splitPerm = permission.split('=') 90 | if (! (splitPerm.length === 2)) { 91 | writeErrorString('Incorrect permission format.', 'Please format your permissions as id=[a][c][r][w].', options) 92 | process.exit(0) 93 | } 94 | let id = splitPerm[0] 95 | const permissionOptions = splitPerm[1].split('') 96 | let type = 'agent'; 97 | 98 | if (options.group) { 99 | type = 'group' 100 | } else if (id === 'p') { 101 | type = 'public' 102 | } else if (id === 'u') { 103 | if (!authenticationInfo.webId) { 104 | writeErrorString('Could not autmatically fill in webId of authenticated user.', 'Please make sure you have an authenticated session to auto-fill your webId', options); 105 | process.exit(0) 106 | } 107 | id = authenticationInfo.webId 108 | } 109 | const read = permissionOptions.indexOf('r') !== -1 110 | const write = permissionOptions.indexOf('w') !== -1 111 | const append = permissionOptions.indexOf('a') !== -1 112 | const control = permissionOptions.indexOf('c') !== -1 113 | const def = options.default 114 | return ({ type, id, read, write, append, control, default: def } as IPermissionOperation) 115 | }) 116 | for (let permission of parsedPermissions) { 117 | const { acp, acl } = await discoverAccessMechanism(url, options.fetch) 118 | if ( !acp && !acl ) { 119 | if (options.verbose) writeErrorString(`Could not set permissions for ${permission.id}`, { message: "Could not find attached WAC or ACP management resource." }, options) 120 | continue; 121 | } 122 | if (acp) { 123 | try { 124 | if (options.group || options.default) throw new Error("Cannot set WAC-specific options such as group and default for non-WAC environments ") 125 | await setPermission(url, [permission], options) 126 | return; 127 | } catch (e) { 128 | writeErrorString(`Could not set permissions for ${permission.id} using ACP`, e, options) 129 | } 130 | } 131 | if (acl) { 132 | try { 133 | await acl_perms.changePermissions(url, [permission], options) 134 | return; 135 | } catch (e) { 136 | writeErrorString(`Could not set permissions for ${permission.id} using WAC`, e, options) 137 | } 138 | } 139 | } 140 | } 141 | catch (e) { 142 | writeErrorString(`Could not evaluate permissions for ${url}`, e, options) 143 | if (this.mayExit) process.exit(1) 144 | } 145 | }) 146 | 147 | access 148 | .command('delete') 149 | .argument('', 'Resource URL') 150 | .description('Delete ACL resource attached to resource with given URI. Does not work for ACP based pods!') 151 | .option('-v, --verbose', 'Log all operations') // Should this be default? 152 | .action(async (url: string, options: any) => { 153 | let programOpts = addEnvOptions(this.programopts || {}); 154 | const authenticationInfo = await authenticate(programOpts) 155 | options.fetch = authenticationInfo.fetch 156 | url = await changeUrlPrefixes(authenticationInfo, url) 157 | 158 | try { 159 | await acl_perms.deletePermissions(url, options) 160 | } catch (e) { 161 | if (options.verbose) writeErrorString(`Could not delete permissions for resource at ${url}`, e, options) 162 | } 163 | if (this.mayExit) process.exit(0) 164 | }) 165 | 166 | return program 167 | } 168 | 169 | } 170 | 171 | 172 | 173 | // todo: unduplicate these functions for universal and ACL 174 | function formatPermissionListing(url: string, permissions: any, options: any) { 175 | let formattedString = `` 176 | let formattedPerms = permissions.access 177 | if (permissions.resource) { 178 | if (permissions.resource.agent) { 179 | for (let agentId of Object.keys(permissions.resource.agent)) { 180 | formattedPerms.agent[agentId]['resource'] = true 181 | } 182 | } 183 | if (permissions.resource.public) { 184 | formattedPerms.public['resource'] = true 185 | } 186 | } 187 | 188 | 189 | if (options.pretty) { 190 | let head = [ 191 | chalk.cyan.bold("ID"), 192 | chalk.bold("read"), 193 | chalk.bold("append"), 194 | chalk.bold("write"), 195 | chalk.bold("control"), 196 | ] 197 | let table = new Table({ head }); 198 | if (!isEmpty(formattedPerms.agent)) { 199 | table.push([chalk.bold('Agent'), '', '', '', '']) 200 | for (let id of Object.keys(formattedPerms.agent)) { 201 | const control = formattedPerms.agent[id].control || (formattedPerms.agent[id].controlRead && formattedPerms.agent[id].controlWrite) 202 | table.push([ 203 | id, 204 | formattedPerms.agent[id].read || 'false', 205 | formattedPerms.agent[id].append || 'false', 206 | formattedPerms.agent[id].write || 'false', 207 | control || 'false', 208 | ]) 209 | } 210 | } 211 | if (!isEmpty(formattedPerms.public)) { 212 | const control = formattedPerms.public.control || (formattedPerms.public.controlRead && formattedPerms.public.controlWrite) 213 | table.push([chalk.bold('Public'), '', '', '', '']) 214 | table.push([ 215 | chalk.blue('#public'), 216 | chalk.bold(formattedPerms.public.read || 'false'), 217 | chalk.bold(formattedPerms.public.append || 'false'), 218 | chalk.bold(formattedPerms.public.write || 'false'), 219 | chalk.bold(control || 'false'), 220 | ]) 221 | } 222 | formattedString += `> ${chalk.bold(url)}\n` 223 | formattedString += `${table.toString()}` 224 | } else { 225 | formattedString += `> ${chalk.bold(url)}\n` 226 | if (!isEmpty(formattedPerms.agent)) { 227 | formattedString += `${chalk.bold('Agent')}\n` 228 | for (let id of Object.keys(formattedPerms.agent)) { 229 | formattedString += `${id} - ` 230 | for (let permission of Object.entries(formattedPerms.agent[id])) { 231 | if (permission[1] && permission[0] !== "controlRead" && permission[0] !== "controlWrite") { 232 | formattedString += `${permission[0]} ` 233 | } 234 | } 235 | if (formattedPerms.agent[id]["controlRead"] && formattedPerms.agent[id]["controlWrite"]) { 236 | formattedString += `control ` 237 | } 238 | formattedString += `\n` 239 | } 240 | } 241 | if (!isEmpty(formattedPerms.public)) { 242 | formattedString += `${chalk.bold('Public')}\n` 243 | formattedString += `${'#public'} - ` 244 | let inherited = true; 245 | for (let permission of Object.entries(formattedPerms.public)) { 246 | if (permission[1] && permission[0] !== "controlRead" && permission[0] !== "controlWrite") { 247 | formattedString += `${permission[0]} ` 248 | } 249 | } 250 | if (formattedPerms.public["controlRead"] && formattedPerms.public["controlWrite"]) { 251 | formattedString += `control ` 252 | } 253 | formattedString += `\n` 254 | } 255 | } 256 | console.log(formattedString) 257 | } 258 | 259 | 260 | function formatACLPermissionListing(url: string, permissions: any, options: any) { 261 | let formattedString = `` 262 | let formattedPerms = permissions.access 263 | if (permissions.resource) { 264 | if (permissions.resource.agent) { 265 | for (let agentId of Object.keys(permissions.resource.agent)) { 266 | formattedPerms.agent[agentId]['resource'] = true 267 | } 268 | } 269 | if (permissions.resource.group) { 270 | for (let groupId of Object.keys(permissions.resource.group)) { 271 | formattedPerms.group[groupId]['resource'] = true 272 | } 273 | } 274 | if (permissions.resource.public) { 275 | formattedPerms.public['resource'] = true 276 | } 277 | } 278 | 279 | if (permissions.default) { 280 | if (permissions.default.agent) { 281 | for (let agentId of Object.keys(permissions.default.agent)) { 282 | formattedPerms.agent[agentId]['default'] = true; 283 | } 284 | } 285 | if (permissions.default.group) { 286 | for (let groupId of Object.keys(permissions.default.group)) { 287 | formattedPerms.group[groupId]['default'] = true; 288 | } 289 | } 290 | if (permissions.default.public) { 291 | let isDefault = true; 292 | for (let value of ["read", "append", "write", "control"]) { 293 | if (permissions.resource.public[value] !== permissions.default.public[value]) { 294 | isDefault = false; 295 | } 296 | } 297 | if (isDefault) formattedPerms.public['default'] = true; 298 | } 299 | } 300 | 301 | 302 | if (options.pretty) { 303 | let head = [ 304 | chalk.cyan.bold("ID"), 305 | chalk.bold("read"), 306 | chalk.bold("append"), 307 | chalk.bold("write"), 308 | chalk.bold("control"), 309 | "inherited", 310 | "is default", 311 | ] 312 | let table = new Table({ head }); 313 | if (!isEmpty(formattedPerms.agent)) { 314 | table.push([chalk.bold('Agent'), '', '', '', '', '', '']) 315 | for (let id of Object.keys(formattedPerms.agent)) { 316 | table.push([ 317 | id, 318 | formattedPerms.agent[id].read || 'false', 319 | formattedPerms.agent[id].append || 'false', 320 | formattedPerms.agent[id].write || 'false', 321 | formattedPerms.agent[id].control || 'false', 322 | formattedPerms.agent[id].resource ? !formattedPerms.agent[id].resource : 'true', // inherited 323 | formattedPerms.agent[id].default || 'false', 324 | ]) 325 | } 326 | } 327 | if (!isEmpty(formattedPerms.group)) { 328 | table.push([chalk.bold('Group'), '', '', '', '', '', '']) 329 | for (let id of Object.keys(formattedPerms.group)) { 330 | table.push([ 331 | chalk.green(id), 332 | formattedPerms.group[id].read || 'false', 333 | formattedPerms.group[id].append || 'false', 334 | formattedPerms.group[id].write || 'false', 335 | formattedPerms.group[id].control || 'false', 336 | formattedPerms.group[id].resource ? !formattedPerms.group[id].resource : 'true', // inherited 337 | formattedPerms.group[id].default || 'false', 338 | ]) 339 | } 340 | } 341 | if (!isEmpty(formattedPerms.public)) { 342 | table.push([chalk.bold('Public'), '', '', '', '', '', '']) 343 | table.push([ 344 | chalk.blue('#public'), 345 | chalk.bold(formattedPerms.public.read || 'false'), 346 | chalk.bold(formattedPerms.public.append || 'false'), 347 | chalk.bold(formattedPerms.public.write || 'false'), 348 | chalk.bold(formattedPerms.public.control || 'false'), 349 | chalk.bold(formattedPerms.public.resource ? !formattedPerms.public.resource : 'true'), // inherited 350 | chalk.bold(formattedPerms.public.default || 'false'), 351 | ]) 352 | 353 | } 354 | formattedString += `> ${chalk.bold(url)}\n` 355 | formattedString += `${table.toString()}` 356 | } else { 357 | formattedString += `> ${chalk.bold(url)}\n` 358 | if (!isEmpty(formattedPerms.agent)) { 359 | formattedString += `${chalk.bold('Agent')}\n` 360 | for (let id of Object.keys(formattedPerms.agent)) { 361 | formattedString += `${id} - ` 362 | let inherited = true; 363 | for (let permission of Object.entries(formattedPerms.agent[id])) { 364 | if (permission[0] === 'resource') { 365 | inherited = false 366 | } else if (permission[1]) { 367 | formattedString += `${permission[0]} ` 368 | } 369 | } 370 | if (inherited) { 371 | formattedString += `${chalk.cyan('inherited')} ` 372 | } 373 | formattedString += `\n` 374 | } 375 | } 376 | if (!isEmpty(formattedPerms.group)) { 377 | formattedString += `${chalk.bold('Group')}\n` 378 | for (let id of Object.keys(formattedPerms.group)) { 379 | formattedString += `${id} - ` 380 | let inherited = true; 381 | for (let permission of Object.entries(formattedPerms.group[id])) { 382 | if (permission[0] === 'resource') { 383 | inherited = false 384 | } else if (permission[1]) { 385 | formattedString += `${permission[0]} ` 386 | } 387 | } 388 | if (inherited) { 389 | formattedString += `${chalk.cyan('inherited')} ` 390 | } 391 | formattedString += `\n` 392 | } 393 | } 394 | if (!isEmpty(formattedPerms.public)) { 395 | formattedString += `${chalk.bold('Public')}\n` 396 | formattedString += `${'#public'} - ` 397 | let inherited = true; 398 | for (let permission of Object.entries(formattedPerms.public)) { 399 | if (permission[0] === 'resource') { 400 | inherited = false 401 | } else if (permission[1]) { 402 | formattedString += `${permission[0]} ` 403 | } 404 | } 405 | if (inherited) { 406 | formattedString += `${chalk.cyan('inherited')} ` 407 | } 408 | formattedString += `\n` 409 | } 410 | } 411 | console.log(formattedString) 412 | } 413 | 414 | function isEmpty (obj: any) { 415 | if (!obj) return true; 416 | return Object.keys(obj).length === 0 417 | } 418 | -------------------------------------------------------------------------------- /src/shell/commands/perms_acl.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import { changePermissions, deletePermissions, listPermissions, IPermissionOperation } from '../../commands/solid-perms_acl'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 5 | import { writeErrorString } from '../../utils/util'; 6 | import chalk from 'chalk'; 7 | import SolidCommand from './SolidCommand'; 8 | const Table = require('cli-table'); 9 | 10 | export default class PermsCommand extends SolidCommand { 11 | 12 | public addCommand(program: Command) { 13 | this.programopts = program.opts(); 14 | 15 | program 16 | .command('chmod') 17 | .description('Utility to list and edit resource permissions on a data pod. Only supports operations on ACL and not ACP.') 18 | .argument('', 'list, edit, delete') 19 | .argument('', 'Resource URL') 20 | .argument('[permissions...]', `Permission operations to edit resource permissions. 21 | Formatted according to id=[d][g][a][c][r][w]. 22 | For public permissions please set id to "p". 23 | For the current authenticated user please set id to "u". 24 | To set updated permissions as default, please add the [d] option as follows: id=d[g][a][c][r][w] 25 | To indicate the id as a group id, please add the [g] option as follows: id=g[d][a][c][r][w] 26 | `) 27 | .option('-p, --pretty', 'Pretty format') 28 | .option('-v, --verbose', 'Log all operations') // Should this be default? 29 | .action(this.executeCommand) 30 | 31 | program 32 | .command('perms') 33 | .description('Utility to list and edit resource permissions on a data pod. Only supports operations on ACL and not ACP.') 34 | .argument('', 'list, edit, delete') 35 | .argument('', 'Resource URL') 36 | .argument('[permissions...]', `Permission operations to edit resource permissions. 37 | Formatted according to id=[d][g][a][c][r][w]. 38 | For public permissions please set id to "p". 39 | For the current authenticated user please set id to "u". 40 | To set updated permissions as default, please add the [d] option as follows: id=d[g][a][c][r][w] 41 | To indicate the id as a group id, please add the [g] option as follows: id=g[d][a][c][r][w] 42 | `) 43 | .option('-p, --pretty', 'Pretty format') 44 | .option('-v, --verbose', 'Log all operations') // Should this be default? 45 | .action(this.executeCommand) 46 | 47 | return program 48 | } 49 | 50 | async executeCommand (operation: string, url: string, permissions: string[], options: any) { 51 | let programOpts = addEnvOptions(this.programopts || {}); 52 | const authenticationInfo = await authenticate(programOpts) 53 | options.fetch = authenticationInfo.fetch 54 | try { 55 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 56 | url = await changeUrlPrefixes(authenticationInfo, url) 57 | 58 | if (operation === 'list') { 59 | let listings = await listPermissions(url, options) 60 | if (listings) formatPermissionListing(url, listings, options) 61 | } else if (operation === 'edit') { 62 | let parsedPermissions = permissions.map(permission => { 63 | const splitPerm = permission.split('=') 64 | if (! (splitPerm.length === 2)) { 65 | writeErrorString('Incorrect permission format.', 'Please format your permissions as id=[d][a][c][r][w].', options) 66 | process.exit(0) 67 | } 68 | let id = splitPerm[0] 69 | const permissionOptions = splitPerm[1].split('') 70 | let type; 71 | if (id === 'p') { 72 | type = 'public' 73 | } else if (id === 'u') { 74 | if (!authenticationInfo.webId) { 75 | writeErrorString('Could not autmatically fill in webId of authenticated user.', 'Please make sure you have an authenticated session to auto-fill your webId', options); 76 | process.exit(0) 77 | } 78 | type = 'agent' 79 | id = authenticationInfo.webId 80 | } else { 81 | type = permissionOptions.indexOf('g') === -1 ? 'agent' : 'group' 82 | } 83 | const read = permissionOptions.indexOf('r') !== -1 84 | const write = permissionOptions.indexOf('w') !== -1 85 | const append = permissionOptions.indexOf('a') !== -1 86 | const control = permissionOptions.indexOf('c') !== -1 87 | const def = permissionOptions.indexOf('d') !== -1 88 | return ({ type, id, read, write, append, control, default: def } as IPermissionOperation) 89 | }) 90 | try { 91 | await changePermissions(url, parsedPermissions, options) 92 | } catch (e) { 93 | if (options.verbose) writeErrorString(`Could not update permissions for resource at ${url}`, e, options) 94 | } 95 | } else if (operation === 'delete') { 96 | try { 97 | await deletePermissions(url, options) 98 | } catch (e) { 99 | if (options.verbose) writeErrorString(`Could not delete permissions for resource at ${url}`, e, options) 100 | } 101 | } else { 102 | console.error('Invalid operation.') 103 | } 104 | } 105 | catch (e) { 106 | writeErrorString(`Could not evaluate permissions for ${url}`, e, options) 107 | if (this.mayExit) process.exit(1) 108 | } 109 | if (this.mayExit) process.exit(0) 110 | } 111 | 112 | } 113 | 114 | 115 | 116 | 117 | function formatPermissionListing(url: string, permissions: any, options: any) { 118 | let formattedString = `` 119 | let formattedPerms = permissions.access 120 | if (permissions.resource) { 121 | if (permissions.resource.agent) { 122 | for (let agentId of Object.keys(permissions.resource.agent)) { 123 | formattedPerms.agent[agentId]['resource'] = true 124 | } 125 | } 126 | if (permissions.resource.group) { 127 | for (let groupId of Object.keys(permissions.resource.group)) { 128 | formattedPerms.group[groupId]['resource'] = true 129 | } 130 | } 131 | if (permissions.resource.public) { 132 | formattedPerms.public['resource'] = true 133 | } 134 | } 135 | 136 | if (permissions.default) { 137 | if (permissions.default.agent) { 138 | for (let agentId of Object.keys(permissions.default.agent)) { 139 | formattedPerms.agent[agentId]['default'] = true; 140 | } 141 | } 142 | if (permissions.default.group) { 143 | for (let groupId of Object.keys(permissions.default.group)) { 144 | formattedPerms.group[groupId]['default'] = true; 145 | } 146 | } 147 | if (permissions.default.public) { 148 | let isDefault = true; 149 | for (let value of ["read", "append", "write", "control"]) { 150 | if (permissions.resource.public[value] !== permissions.default.public[value]) { 151 | isDefault = false; 152 | } 153 | } 154 | if (isDefault) formattedPerms.public['default'] = true; 155 | } 156 | } 157 | 158 | 159 | if (options.pretty) { 160 | let head = [ 161 | chalk.cyan.bold("ID"), 162 | chalk.bold("read"), 163 | chalk.bold("append"), 164 | chalk.bold("write"), 165 | chalk.bold("control"), 166 | "inherited", 167 | "is default", 168 | ] 169 | let table = new Table({ head }); 170 | if (!isEmpty(formattedPerms.agent)) { 171 | table.push([chalk.bold('Agent'), '', '', '', '', '', '']) 172 | for (let id of Object.keys(formattedPerms.agent)) { 173 | table.push([ 174 | id, 175 | formattedPerms.agent[id].read || 'false', 176 | formattedPerms.agent[id].append || 'false', 177 | formattedPerms.agent[id].write || 'false', 178 | formattedPerms.agent[id].control || 'false', 179 | formattedPerms.agent[id].resource ? !formattedPerms.agent[id].resource : 'true', // inherited 180 | formattedPerms.agent[id].default || 'false', 181 | ]) 182 | } 183 | } 184 | if (!isEmpty(formattedPerms.group)) { 185 | table.push([chalk.bold('Group'), '', '', '', '', '', '']) 186 | for (let id of Object.keys(formattedPerms.group)) { 187 | table.push([ 188 | chalk.green(id), 189 | formattedPerms.group[id].read || 'false', 190 | formattedPerms.group[id].append || 'false', 191 | formattedPerms.group[id].write || 'false', 192 | formattedPerms.group[id].control || 'false', 193 | formattedPerms.group[id].resource ? !formattedPerms.group[id].resource : 'true', // inherited 194 | formattedPerms.group[id].default || 'false', 195 | ]) 196 | } 197 | } 198 | if (!isEmpty(formattedPerms.public)) { 199 | table.push([chalk.bold('Public'), '', '', '', '', '', '']) 200 | table.push([ 201 | chalk.blue('#public'), 202 | chalk.bold(formattedPerms.public.read || 'false'), 203 | chalk.bold(formattedPerms.public.append || 'false'), 204 | chalk.bold(formattedPerms.public.write || 'false'), 205 | chalk.bold(formattedPerms.public.control || 'false'), 206 | chalk.bold(formattedPerms.public.resource ? !formattedPerms.public.resource : 'true'), // inherited 207 | chalk.bold(formattedPerms.public.default || 'false'), 208 | ]) 209 | 210 | } 211 | formattedString += `> ${chalk.bold(url)}\n` 212 | formattedString += `${table.toString()}` 213 | } else { 214 | formattedString += `> ${chalk.bold(url)}\n` 215 | if (!isEmpty(formattedPerms.agent)) { 216 | formattedString += `${chalk.bold('Agent')}\n` 217 | for (let id of Object.keys(formattedPerms.agent)) { 218 | formattedString += `${id} - ` 219 | let inherited = true; 220 | for (let permission of Object.entries(formattedPerms.agent[id])) { 221 | if (permission[0] === 'resource') { 222 | inherited = false 223 | } else if (permission[1]) { 224 | formattedString += `${permission[0]} ` 225 | } 226 | } 227 | if (inherited) { 228 | formattedString += `${chalk.cyan('inherited')} ` 229 | } 230 | formattedString += `\n` 231 | } 232 | } 233 | if (!isEmpty(formattedPerms.group)) { 234 | formattedString += `${chalk.bold('Group')}\n` 235 | for (let id of Object.keys(formattedPerms.group)) { 236 | formattedString += `${id} - ` 237 | let inherited = true; 238 | for (let permission of Object.entries(formattedPerms.group[id])) { 239 | if (permission[0] === 'resource') { 240 | inherited = false 241 | } else if (permission[1]) { 242 | formattedString += `${permission[0]} ` 243 | } 244 | } 245 | if (inherited) { 246 | formattedString += `${chalk.cyan('inherited')} ` 247 | } 248 | formattedString += `\n` 249 | } 250 | } 251 | if (!isEmpty(formattedPerms.public)) { 252 | formattedString += `${chalk.bold('Public')}\n` 253 | formattedString += `${'#public'} - ` 254 | let inherited = true; 255 | for (let permission of Object.entries(formattedPerms.public)) { 256 | if (permission[0] === 'resource') { 257 | inherited = false 258 | } else if (permission[1]) { 259 | formattedString += `${permission[0]} ` 260 | } 261 | } 262 | if (inherited) { 263 | formattedString += `${chalk.cyan('inherited')} ` 264 | } 265 | formattedString += `\n` 266 | } 267 | } 268 | console.log(formattedString) 269 | } 270 | 271 | function isEmpty (obj: any) { 272 | return Object.keys(obj).length === 0 273 | } 274 | -------------------------------------------------------------------------------- /src/shell/commands/query.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import query, { queryFederated } from '../../commands/solid-query'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 5 | import { writeErrorString } from '../../utils/util'; 6 | import fs from 'fs'; 7 | import chalk from 'chalk'; 8 | import SolidCommand from './SolidCommand'; 9 | const Table = require('cli-table'); 10 | 11 | export default class QueryCommand extends SolidCommand { 12 | 13 | public addCommand(program: Command) { 14 | this.programopts = program.opts(); 15 | 16 | program 17 | .command('query') 18 | .description('Query RDF resoures using SPARQL') 19 | .argument('', 'Resource to query. In case of container recursively queries all contained files.') 20 | .argument('', 'SPARQL query string | file path containing SPARQL query when -q flag is active') 21 | .option('-a, --all', 'Match .acl and .meta files') 22 | .option('-q, --queryfile', 'Process query parameter as file path of SPARQL query') 23 | .option('-p, --pretty', 'Pretty format') 24 | .option('-f, --full', 'Return containing files using full filename.') 25 | .option('-F, --federated', 'Evaluate query over combined contents of all resources in container tree') 26 | .option('-v, --verbose', 'Log all operations') // Should this be default? 27 | .action(async (url, queryString, options) => { 28 | let programOpts = addEnvOptions(program.opts() || {}); 29 | const authenticationInfo = await authenticate(programOpts) 30 | options.fetch = authenticationInfo.fetch 31 | try { 32 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 33 | url = await changeUrlPrefixes(authenticationInfo, url) 34 | if (options.queryfile) { 35 | queryString = fs.readFileSync(queryString, { encoding: "utf-8" }) 36 | } 37 | if (options.federated) { 38 | formatBindings("Federated query", await queryFederated(url, queryString, options), options) 39 | } else { 40 | for await (let result of query(url, queryString, options)) { 41 | formatBindings(result.fileName, result.bindings, options) 42 | } 43 | } 44 | } catch (e) { 45 | writeErrorString(`Could not query resource at ${url}`, e, options) 46 | if (this.mayExit) process.exit(1) 47 | } 48 | if (this.mayExit) process.exit(0) 49 | }) 50 | 51 | return program 52 | } 53 | } 54 | 55 | 56 | 57 | function formatBindings(fileName: string, bindings: any, options: any) { 58 | if (options.pretty) { 59 | let table; 60 | if (!bindings.length) { 61 | if (options.verbose) console.log(chalk.bold(`> ${fileName}`)) 62 | if (options.verbose) writeErrorString(`No results for resource ${fileName}`, '-', options) 63 | return; 64 | } 65 | for (let binding of bindings) { 66 | if (!table) { 67 | table = new Table({ 68 | head: Array.from(bindings[0].entries.keys()) 69 | }); 70 | } 71 | table.push(Array.from(binding.entries.values()).map(e => (e as any).value || '')) 72 | } 73 | console.log(` 74 | ${chalk.bold(`> ${fileName}`)} 75 | ${table.toString()} 76 | `) 77 | } else { 78 | let bindingsString = "" 79 | if (!bindings.length) { 80 | if (options.verbose) console.log(chalk.bold(`> ${fileName}`)) 81 | if (options.verbose) writeErrorString(`No results for resource ${fileName}`, '-', options) 82 | return; 83 | } 84 | for (let binding of bindings) { 85 | for (let entry of Array.from(binding.entries.entries())) { 86 | bindingsString += `${(entry as any)[0]}: ${(entry as any)[1].value}\n` 87 | } 88 | bindingsString += `\n` 89 | } 90 | console.log(`${chalk.bold(`> ${fileName}`)}\n${bindingsString}`) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/shell/commands/remove.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import remove from '../../commands/solid-remove'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 5 | import { writeErrorString } from '../../utils/util'; 6 | import SolidCommand from './SolidCommand'; 7 | 8 | export default class RemoveCommand extends SolidCommand { 9 | 10 | public addCommand(program: Command) { 11 | this.programopts = program.opts(); 12 | 13 | program 14 | .command('rm') 15 | .description('Remove files and container') 16 | .argument('', 'URL of container to be listed') 17 | .option('-r, --recursive', 'Recursively removes all files in given container (.acl files are removed on resource removal)') // Should this be default? 18 | .option('-v, --verbose', 'Log all operations') // Should this be default? 19 | .action(this.executeCommand) 20 | 21 | return program 22 | } 23 | 24 | async executeCommand (urls: string[], options: any) { 25 | let programOpts = addEnvOptions(this.programopts || {}); 26 | const authenticationInfo = await authenticate(programOpts) 27 | options.fetch = authenticationInfo.fetch 28 | for (let url of urls) { 29 | try { 30 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 31 | url = await changeUrlPrefixes(authenticationInfo, url) 32 | await remove(url, options) 33 | } catch (e) { 34 | writeErrorString(`Could not remove ${url}`, e, options) 35 | } 36 | } 37 | if (this.mayExit) process.exit(0) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/shell/commands/shell.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import shell from '../../commands/solid-shell'; 3 | import { writeErrorString } from '../../utils/util'; 4 | import SolidCommand from './SolidCommand'; 5 | 6 | export default class ShellCommand extends SolidCommand { 7 | 8 | public addCommand(program: Command) { 9 | // this.programopts = program.opts(); 10 | 11 | // program 12 | // .command('shell') 13 | // .description('Open a Solid Shell') 14 | // .action(async () => { 15 | // try { 16 | // await shell(program.opts()); 17 | // } catch (e) { 18 | // writeErrorString(`Could not open Solid Shell`, e, options) 19 | // if (this.mayExit) process.exit(1) 20 | // } 21 | // if (this.mayExit) process.exit(0) 22 | // }) 23 | 24 | return program 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/shell/commands/touch.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import touch from '../../commands/solid-touch'; 3 | import authenticate from '../../authentication/authenticate'; 4 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 5 | import { writeErrorString } from '../../utils/util'; 6 | import SolidCommand from './SolidCommand'; 7 | 8 | export default class TouchCommand extends SolidCommand { 9 | 10 | public addCommand(program: Command) { 11 | this.programopts = program.opts(); 12 | 13 | program 14 | .command('touch') 15 | .description('Create an empty resource') 16 | .argument('', 'resource to be created') 17 | .option('-c, --content-type ', 'Content type of the created resource') 18 | .option('-v, --verbose', 'Log all operations') 19 | .action(async (url, options) => { 20 | let programOpts = addEnvOptions(program.opts() || {}); 21 | const authenticationInfo = await authenticate(programOpts) 22 | options.fetch = authenticationInfo.fetch 23 | try { 24 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 25 | url = await changeUrlPrefixes(authenticationInfo, url) 26 | await touch(url, options) 27 | } catch (e) { 28 | writeErrorString(`Could not touch ${url}`, e, options) 29 | if (this.mayExit) process.exit(1) 30 | } 31 | if (this.mayExit) process.exit(0) 32 | }) 33 | 34 | return program 35 | } 36 | } -------------------------------------------------------------------------------- /src/shell/commands/tree.ts: -------------------------------------------------------------------------------- 1 | import authenticate from '../../authentication/authenticate'; 2 | import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; 3 | import { writeErrorString } from '../../utils/util'; 4 | import tree from '../../commands/solid-tree'; 5 | import { Command } from 'commander'; 6 | import SolidCommand from './SolidCommand'; 7 | 8 | export default class TreeCommand extends SolidCommand { 9 | 10 | public addCommand(program: Command) { 11 | this.programopts = program.opts(); 12 | let urlParam = this.mayUseCurrentContainer ? '[url]' : '' 13 | 14 | program 15 | .command('tree') 16 | .description('View resource tree from container') 17 | .argument(urlParam, 'Base container to construct tree over') 18 | .option('-a, --all', 'Display .acl, .acp and .meta resources') 19 | .option('-f, --full', 'Display full resource URIs') 20 | .option('-v, --verbose', 'Log all operations') // Should this be default? 21 | .action(async (url: string, options: any) => { 22 | await this.executeCommand(url, options) 23 | }) 24 | 25 | return program 26 | } 27 | 28 | private async executeCommand (url?: string, options?: any) { 29 | let programOpts = addEnvOptions(this.programopts || {}); 30 | const authenticationInfo = await authenticate(programOpts) 31 | options.fetch = authenticationInfo.fetch 32 | try { 33 | if (!url) url = this.shell?.workingContainer || undefined; 34 | if (this.shell) url = getAndNormalizeURL(url, this.shell); 35 | if (!url) throw new Error('No valid url parameter passed') 36 | url = await changeUrlPrefixes(authenticationInfo, url) 37 | await tree(url, options) 38 | } catch (e) { 39 | writeErrorString(`Could not display tree structure for ${url}`, e, options) 40 | if (this.mayExit) process.exit(1) 41 | } 42 | if (this.mayExit) process.exit(0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/shell/shellcommands.ts: -------------------------------------------------------------------------------- 1 | import ChangedirectoryCommand from './commands/navigation/cd'; 2 | import FetchCommand from "./commands/fetch"; 3 | import ListCommand from './commands/list'; 4 | import CopyCommand from './commands/copy'; 5 | import MoveCommand from './commands/mv'; 6 | import RemoveCommand from './commands/remove'; 7 | import FindCommand from './commands/find'; 8 | import QueryCommand from './commands/query'; 9 | import EditCommand from './commands/edit'; 10 | import MkdirCommand from './commands/mkdir'; 11 | import PermsCommand from './commands/perms'; 12 | import ShellCommand from './commands/shell'; 13 | import TouchCommand from './commands/touch'; 14 | import TreeCommand from './commands/tree'; 15 | import ExitCommand from './commands/exit'; 16 | import AuthCommand from './commands/auth'; 17 | 18 | import CreatePodCommand from './commands/css-specific/create-pod'; 19 | 20 | export { FetchCommand, ListCommand, CopyCommand, MoveCommand, RemoveCommand, FindCommand, QueryCommand, EditCommand, MkdirCommand, PermsCommand, ShellCommand, TouchCommand, TreeCommand, ExitCommand, ChangedirectoryCommand, AuthCommand, CreatePodCommand } -------------------------------------------------------------------------------- /src/utils/authenticationUtils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path' 3 | import { KeyPair } from '@inrupt/solid-client-authn-core'; 4 | import { KeyLike, JWK, importJWK, exportJWK } from 'jose'; 5 | import jwt_decode from 'jwt-decode'; 6 | import { randomUUID } from 'crypto'; 7 | import { setConfigSession, getConfigCurrentSession } from './configoptions'; 8 | import crossfetch from 'cross-fetch'; 9 | import type { Logger } from '../logger'; 10 | 11 | export type SessionInfo = { 12 | fetch: typeof fetch 13 | webId?: string 14 | } 15 | 16 | export type OIDCConfig = { 17 | authorization_endpoint: string, 18 | claims_parameter_supported: true, 19 | claims_supported: string[], 20 | code_challenge_methods_supported: string[], 21 | end_session_endpoint: string, 22 | grant_types_supported: string[], 23 | id_token_signing_alg_values_supported: string[], 24 | issuer: string, 25 | jwks_uri: string, 26 | registration_endpoint: string, 27 | response_modes_supported: string[], 28 | response_types_supported: string[], 29 | scopes_supported: string[], 30 | subject_types_supported: string[], 31 | token_endpoint_auth_methods_supporte: string[], 32 | token_endpoint_auth_signing_alg_values_supported: string[], 33 | token_endpoint: string, 34 | request_object_signing_alg_values_supported: string[], 35 | request_parameter_supported: boolean, 36 | request_uri_parameter_supported: boolean, 37 | require_request_uri_registration: boolean, 38 | userinfo_endpoint: string, 39 | userinfo_signing_alg_values_supported: string[], 40 | introspection_endpoint: string, 41 | introspection_endpoint_auth_methods_supported: string[], 42 | introspection_endpoint_auth_signing_alg_values_supported: string[], 43 | dpop_signing_alg_values_supported: string[], 44 | revocation_endpoint: string, 45 | revocation_endpoint_auth_methods_supported: string[], 46 | revocation_endpoint_auth_signing_alg_values_supported: string[], 47 | claim_types_supported: string[], 48 | solid_oidc_supported: string, 49 | } 50 | 51 | export async function getOIDCConfig(idp: string): Promise { 52 | try { 53 | idp = idp.endsWith('/') ? idp : idp + '/'; 54 | let oidclocation = idp + '.well-known/openid-configuration' 55 | let res = await crossfetch(oidclocation) 56 | if (!res.ok) throw new Error(`HTTP Error Response requesting ${oidclocation}: ${res.status} ${res.statusText}`); 57 | let json = await res.json(); 58 | return json; 59 | } catch (e) { 60 | let message = (e instanceof Error) ? e.message : String(e); 61 | throw new Error(`Could not find OIDC config of user: ${message}`) 62 | } 63 | } 64 | 65 | export function ensureDirectoryExistence(filePath: string) { 66 | var dirname = path.dirname(filePath); 67 | if (fs.existsSync(dirname)) { 68 | return true; 69 | } 70 | ensureDirectoryExistence(dirname); 71 | fs.mkdirSync(dirname); 72 | } 73 | 74 | 75 | type ISessionInformation = { 76 | clientId?: string, 77 | idTokenSignedResponseAlg?: string, 78 | clientSecret?: string, 79 | issuer?: string, 80 | redirectUrl?: string, 81 | dpop?: string, 82 | refreshToken?: string, 83 | webId?: string, 84 | isLoggedIn?: string, 85 | publicKey?: string, 86 | privateKey?: string, 87 | sessionId?: string, 88 | } 89 | 90 | export async function getSessionInfoFromStorage(storage: StorageHandler) : Promise { 91 | let foundSessionInfo : ISessionInformation | undefined; 92 | 93 | let sessions = await storage.get('solidClientAuthn:registeredSessions') 94 | if (sessions) { 95 | let parsedSessionObjects = JSON.parse(sessions) 96 | for (let sessionNumber of parsedSessionObjects || []) { 97 | let sessionInfo: any = await storage.get(`solidClientAuthenticationUser:${sessionNumber}`) 98 | if (sessionInfo) { 99 | foundSessionInfo = JSON.parse(sessionInfo) as ISessionInformation 100 | foundSessionInfo.sessionId = sessionNumber 101 | } 102 | } 103 | } 104 | return foundSessionInfo 105 | } 106 | 107 | export class StorageHandler { 108 | map: Map; 109 | constructor() { 110 | this.map = new Map() 111 | } 112 | public get = async (key: string) : Promise => { 113 | return this.map.get(key) 114 | } 115 | public set = async (key: string, value: string) : Promise => { 116 | this.map.set(key, value) 117 | return 118 | } 119 | public delete = async (key: string) : Promise => { 120 | this.map.delete(key) 121 | } 122 | 123 | public async writeToFile(filePath: string) { 124 | // TODO:: FIx this encoding sequence. This is POC 125 | // let PASSPHRASE = MACADDRESS 126 | try { 127 | let text = JSON.stringify(Array.from(this.map)) 128 | // var encrypted = CryptoJS.AES.encrypt(text, PASSPHRASE); 129 | await ensureDirectoryExistence(filePath) 130 | if (!fs.existsSync(filePath)){ 131 | fs.mkdirSync(filePath, { recursive: true }); 132 | } 133 | // fs.writeFileSync(filePath, encrypted.toString()) 134 | fs.writeFileSync(filePath, text) 135 | } catch (e) { 136 | throw new Error('Could not write credentials file.') 137 | } 138 | } 139 | 140 | public loadFromFile(fileName: string) { 141 | // TODO:: FIx this decoding sequence. This is POC 142 | // let PASSPHRASE = MACADDRESS 143 | try { 144 | let text = fs.readFileSync(fileName, {encoding: 'utf8'}) 145 | // var decrypted = CryptoJS.AES.decrypt(text, PASSPHRASE); 146 | // this.map = new Map(JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))) 147 | this.map = new Map(JSON.parse(text)) 148 | } catch (e) { 149 | throw new Error('Could not read credentials file.') 150 | } 151 | } 152 | 153 | public clear() { 154 | this.map = new Map(); 155 | } 156 | 157 | } 158 | 159 | 160 | const JWTALG = 'ES256'; 161 | type SessionTokenInfo = { 162 | accessToken: string, 163 | expirationDate: Date, 164 | dpopKey: KeyPair 165 | webId?: string, 166 | idp?: string, 167 | } 168 | 169 | export async function storeSessionTokenInfo(accessToken: string, dpopKey: KeyPair, expirationDate: Date, webId?: string, idp?: string) { 170 | let id = randomUUID(); 171 | let privateKeyJWK = await exportJWK(dpopKey.privateKey) 172 | let expirationDateString = expirationDate.toISOString(); 173 | let exportedObject = { id, accessToken, dpopKey: { privateKey: privateKeyJWK, publicKey: dpopKey.publicKey }, expirationDate: expirationDateString, webId, idp } 174 | // Add the session to the config 175 | if (webId) { 176 | setConfigSession(webId, exportedObject); 177 | } 178 | 179 | return; 180 | } 181 | 182 | export async function readSessionTokenInfo() : Promise { 183 | let sessionInfo: SessionTokenInfo = getConfigCurrentSession() as unknown as SessionTokenInfo; // TODO:: Double check this? 184 | sessionInfo.expirationDate = new Date(sessionInfo.expirationDate); 185 | sessionInfo.dpopKey = await fixKeyPairType(sessionInfo.dpopKey); 186 | return sessionInfo; 187 | } 188 | 189 | 190 | async function fixKeyPairType(key: any) : Promise { 191 | let publicKeyJWK : JWK; 192 | let privateKeyKeyLike : KeyLike; 193 | try { 194 | const publicKeyKeyLike = await importJWK(key.publicKey, JWTALG); 195 | publicKeyJWK = await exportJWK(publicKeyKeyLike); 196 | privateKeyKeyLike = await importJWK(key.privateKey, JWTALG) as KeyLike; 197 | } catch (e) { 198 | let message = (e instanceof Error) ? e.message : String(e); 199 | throw new Error(`Cannot restore session keys: ${message}`) 200 | } 201 | let dpopKey = { 202 | privateKey: privateKeyKeyLike, 203 | publicKey: publicKeyJWK 204 | }; 205 | dpopKey.publicKey.alg = JWTALG; 206 | return dpopKey; 207 | 208 | } 209 | 210 | type IdToken = { 211 | azp: string, 212 | sub: string, 213 | webid: string, 214 | at_hash: string, 215 | aud: string, 216 | exp: number, 217 | iat: number, 218 | iss: string, 219 | 220 | } 221 | export function decodeIdToken(idToken: string): IdToken { 222 | return jwt_decode(idToken); 223 | } 224 | 225 | export function writeErrorString(explanation: string, e: any, options?: { logger?: Logger }) { 226 | let message = (e instanceof Error) ? e.message : String(e); 227 | (options?.logger || console).error(`${explanation}: ${message}`) 228 | } -------------------------------------------------------------------------------- /src/utils/configoptions.ts: -------------------------------------------------------------------------------- 1 | import { getSolidDataset, getThing, getUrl, getUrlAll } from '@inrupt/solid-client'; 2 | const fs = require('fs') 3 | 4 | const homedir = require('os').homedir(); 5 | const SOLIDDIR = `${homedir}/.solid/` 6 | const BASHLIBCONFIGPATH = `${SOLIDDIR}.bashlibconfig` 7 | 8 | export type IConfig = { 9 | currentWebID?: string, 10 | authInfo: Record 11 | } 12 | 13 | export type IAuthInfoEntry = { 14 | token?: ITokenEntry, 15 | session?: ISessionEntry, 16 | } 17 | 18 | export type ITokenEntry = { 19 | name?: string, 20 | email?: string, 21 | idp: string, 22 | webId: string, 23 | id: string, 24 | secret?: string, 25 | } 26 | 27 | export type ISessionEntry = { 28 | id: string, 29 | idp: string, 30 | webId: string, 31 | expirationDate: Date, 32 | } 33 | 34 | export function initializeConfig() { 35 | if (!fs.existsSync(SOLIDDIR)) { 36 | fs.mkdirSync(SOLIDDIR) 37 | } 38 | if (!fs.existsSync(BASHLIBCONFIGPATH)) { 39 | let config: IConfig = { 40 | currentWebID: undefined, 41 | authInfo: { 42 | 43 | } 44 | } 45 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 46 | } 47 | } 48 | 49 | export async function checkValidWebID(webId: string | undefined) { 50 | if (!webId) return false; 51 | let webIdDocumentURL = webId.split('#')[0] 52 | try { 53 | let ds = await getSolidDataset(webId) 54 | let documentThing = getThing(ds, webIdDocumentURL) 55 | let webIdThing = getThing(ds, webId) 56 | if (documentThing && getUrlAll(documentThing, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type").includes('http://xmlns.com/foaf/0.1/PersonalProfileDocument')) { 57 | return true; 58 | } else if (webIdThing && getUrl(webIdThing, "http://www.w3.org/ns/solid/terms#oidcIssuer")) { 59 | return true; 60 | } 61 | 62 | } catch (_ignored) { 63 | return false; 64 | } 65 | return false; 66 | } 67 | 68 | export async function clearConfigCurrentWebID() { 69 | try { 70 | let config = loadConfig() 71 | delete config.currentWebID 72 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 73 | } 74 | catch (e) { 75 | throw new Error('Could not read config.') 76 | } 77 | } 78 | 79 | 80 | export async function setConfigCurrentWebID(webId: string | undefined) { 81 | if (!webId) throw new Error(`No WebID value provided`) 82 | let valid = await checkValidWebID(webId) 83 | if (!valid) throw new Error(`Invalid WebID value provided: "${webId}"`) 84 | 85 | try { 86 | let config = loadConfig() 87 | config.currentWebID = webId 88 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 89 | } 90 | catch (e) { 91 | throw new Error('Could not read config.') 92 | } 93 | } 94 | 95 | export function getConfigCurrentWebID() { 96 | try { 97 | let config : IConfig = JSON.parse(fs.readFileSync(BASHLIBCONFIGPATH, { encoding: "utf8" })); 98 | return config.currentWebID; 99 | } 100 | catch (e) { 101 | throw new Error('Could not read config.') 102 | } 103 | } 104 | 105 | export function setConfigToken(webId: string, token: any) { 106 | try { 107 | let config = loadConfig() 108 | if (config.authInfo[webId]) { 109 | if (config.authInfo[webId].token) { 110 | console.error('WebID already has token entry') 111 | } else { 112 | config.authInfo[webId].token = token 113 | // Remove any prior session for interactive authentication. 114 | config.authInfo[webId].session = undefined 115 | } 116 | } else { 117 | config.authInfo[webId] = { 118 | token: token 119 | } 120 | } 121 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 122 | } 123 | catch (e) { 124 | throw new Error('Could not read config.') 125 | } 126 | } 127 | 128 | export function setConfigSession(webId: string, session: any) { 129 | try { 130 | let config = loadConfig() 131 | if (config.authInfo[webId]) { 132 | config.authInfo[webId].session = session 133 | } else { 134 | config.authInfo[webId] = { session: session } 135 | } 136 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 137 | } 138 | catch (e) { 139 | throw new Error('Could not read config.') 140 | } 141 | } 142 | 143 | export function getConfigCurrentSession() { 144 | let config : IConfig = JSON.parse(fs.readFileSync(BASHLIBCONFIGPATH, { encoding: "utf8" })); 145 | let webId = config.currentWebID 146 | if (!webId) return null; 147 | return config.authInfo[webId].session 148 | } 149 | 150 | export function getConfigCurrentToken() { 151 | let config : IConfig = JSON.parse(fs.readFileSync(BASHLIBCONFIGPATH, { encoding: "utf8" })); 152 | let webId = config.currentWebID 153 | if (!webId) return null; 154 | return config.authInfo[webId]?.token 155 | } 156 | 157 | export function getAllConfigEntries() { 158 | let info: Record = {} 159 | try { 160 | let config = loadConfig() 161 | for (let webId of Object.keys(config.authInfo)) { 162 | info[webId] = { 163 | hasToken: !!config.authInfo[webId].token, 164 | session: config.authInfo[webId].session && { 165 | id: config.authInfo[webId].session?.id, 166 | idp: config.authInfo[webId].session?.idp, 167 | expirationDate: config.authInfo[webId].session?.expirationDate 168 | } 169 | } 170 | } 171 | } 172 | catch (e) { 173 | throw new Error('Could not read config.') 174 | } 175 | return info 176 | } 177 | 178 | export function removeConfigSession(webId: string) { 179 | try { 180 | let config : IConfig = JSON.parse(fs.readFileSync(BASHLIBCONFIGPATH, { encoding: "utf8" })); 181 | delete config.authInfo[webId]; 182 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 183 | } 184 | catch (e) { 185 | throw new Error('Could not read config.') 186 | } 187 | } 188 | 189 | export function removeConfigSessionAll() { 190 | try { 191 | let config = loadConfig() 192 | for (let webId of Object.keys(config.authInfo)) { 193 | delete config.authInfo[webId] 194 | } 195 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 196 | } 197 | catch (e) { 198 | throw new Error('Could not read config.') 199 | } 200 | } 201 | 202 | export function addConfigEmtpyEntry(webId: string) { 203 | try { 204 | let config = loadConfig() 205 | if (!config.authInfo[webId]) { 206 | config.authInfo[webId] = {}; 207 | } 208 | fs.writeFileSync(BASHLIBCONFIGPATH, JSON.stringify(config, null, 2)) 209 | } 210 | catch (e) { 211 | throw new Error('Could not read config.') 212 | } 213 | } 214 | 215 | function loadConfig() : IConfig { 216 | let config : IConfig = JSON.parse(fs.readFileSync(BASHLIBCONFIGPATH, { encoding: "utf8" })); 217 | for (let webId of Object.keys(config.authInfo)) { 218 | let expirationDate = config.authInfo[webId].session?.expirationDate 219 | if (expirationDate) 220 | (config.authInfo[webId].session as any).expirationDate = new Date(expirationDate) // idk why its complaining here 221 | } 222 | return config 223 | } 224 | -------------------------------------------------------------------------------- /src/utils/errors/BashlibError.ts: -------------------------------------------------------------------------------- 1 | export enum BashlibErrorMessage { 2 | noWebIDOption = "No WebID option", 3 | noIDPOption = "No Identity Provider option", 4 | invalidIDPOption = "Invalid Identity Provider option", 5 | authFlowError = "Authentication flow error", 6 | cannotRestoreSession = "Cannot restore previous session", 7 | cannotCreateSession = "Could not create an authenticated session.", 8 | noValidToken = "No valid authentication token found.", 9 | httpResponseError = "HTTP Error Response requesting", 10 | cannotWriteResource = "Cannot write resource" 11 | } 12 | 13 | export default class BashlibError extends Error{ 14 | constructor(message: BashlibErrorMessage, value?: string, errorMessage?: string) { 15 | let fullMessage = message.toString() 16 | if (value) fullMessage += ` "${value}"` 17 | if (errorMessage) fullMessage += ` : ${errorMessage}` 18 | fullMessage += '.' 19 | super(fullMessage) 20 | } 21 | } -------------------------------------------------------------------------------- /src/utils/shellutils.ts: -------------------------------------------------------------------------------- 1 | import { getInbox, getPodRoot, writeErrorString } from './util'; 2 | import { SolidShell } from '../commands/solid-shell'; 3 | 4 | export function arrayifyHeaders(value: any, previous: any) { return previous ? previous.concat(value) : [value] } 5 | 6 | /** 7 | * General optionsL 8 | * 9 | * requestAuth // Disable authentication requesting 10 | * idp // Identity Provider 11 | * config // Path of the config file 12 | * port // Port to handle redirect from login 13 | * verbose // Enable verbose setting 14 | */ 15 | 16 | enum AuthTypes { 17 | "token", 18 | "interactive", 19 | "request", 20 | "none" 21 | } 22 | 23 | export function addEnvOptions(options: any) { 24 | // Set environment variables 25 | let envOptions: any = { 26 | auth: process.env['BASHLIB_AUTH'] || undefined, 27 | idp: process.env['BASHLIB_IDP'] || undefined, 28 | config: process.env['BASHLIB_CONFIG'] || undefined, 29 | port: process.env['BASHLIB_PORT'] || undefined, 30 | } 31 | 32 | if (options.auth) { 33 | try { 34 | options.auth = options.auth as AuthTypes 35 | } catch (e) { 36 | options.auth = undefined; 37 | } 38 | } 39 | 40 | // cleanup undefined values for merging with spread operator. 41 | Object.keys(envOptions).forEach(key => envOptions[key] === undefined ? delete envOptions[key] : {}); 42 | return { ... options, ... envOptions} 43 | } 44 | 45 | function prepareOptions(options: any) { 46 | let processOptions = {} 47 | 48 | return processOptions 49 | } 50 | 51 | 52 | export async function changeUrlPrefixes(authenticationInfo: any, url: string) { 53 | if (!url) return url; 54 | 55 | if (url.startsWith('webid:')) { 56 | if (!authenticationInfo.webId) throw new Error('Cannot process URL with "webid:" prefix, no WebID value currently known.') 57 | return authenticationInfo.webId as string 58 | 59 | } else if (url.startsWith('root:')) { 60 | if (!authenticationInfo.webId) throw new Error('Cannot process URL with "root:" prefix, no WebID value currently known.') 61 | let podRoot = await getPodRoot(authenticationInfo.webId, authenticationInfo.fetch); 62 | if (!podRoot) throw new Error('No pod root container found') 63 | return mergeStringsSingleSlash(podRoot, url.replace('root:', '')) 64 | 65 | } else if (url.startsWith('base:')) { 66 | if (!authenticationInfo.webId) throw new Error('Cannot process URL with "base:" prefix, no WebID value currently known.') 67 | let podRoot = await getPodRoot(authenticationInfo.webId, authenticationInfo.fetch); 68 | if (!podRoot) throw new Error('No pod root container found') 69 | return mergeStringsSingleSlash(podRoot, url.replace('base:', '')) 70 | 71 | } else if (url.startsWith('inbox:')) { 72 | if (!authenticationInfo.webId) throw new Error('Cannot process URL with "inbox:" prefix, no WebID value currently known.') 73 | let inbox = await getInbox(authenticationInfo.webId, authenticationInfo.fetch); 74 | if (!inbox) throw new Error('No inbox value found') 75 | return mergeStringsSingleSlash(inbox, url.replace('inbox:', '')) 76 | 77 | } else { 78 | return url; 79 | } 80 | } 81 | 82 | export function mergeStringsSingleSlash(a: string, b: string) { 83 | if (!b && a && !a.endsWith('/')) return a + '/' 84 | if (a.endsWith('/') && b.startsWith('/')) { 85 | return `${a}${b.slice(1).toString()}` 86 | } 87 | if (!a.endsWith('/') && !b.startsWith('/')) { 88 | return `${a}/${b}` 89 | } 90 | return `${a}${b}` 91 | } 92 | 93 | export function normalizeURL(url: string, shell?: SolidShell): string { 94 | if (url.startsWith('http') || url.startsWith('https')) { 95 | return url; 96 | } else if (shell && (shell.podBaseURI && shell.workingContainer)) { 97 | if (url.startsWith('/') && !shell.podBaseURI) throw new Error('Cannot find root of the current Solid pod.') 98 | let path = url.startsWith('/') ? shell.podBaseURI : shell.workingContainer 99 | for (let pathEntry of url.split('/')) { 100 | if (pathEntry === '.') { 101 | continue; 102 | } else if (pathEntry === '..') { 103 | let split = path.split('/') 104 | path = path.endsWith('/') ? split.slice(0, split.length - 2).join('/') : split.slice(0, split.length - 1).join('/') 105 | path = path + '/' // Prevent missing trailing slash 106 | } else if (pathEntry === '*') { 107 | throw new Error(`Wildcard urls ('*') are currently not supported`) 108 | } else { 109 | if (path.endsWith('/') && !pathEntry) { 110 | continue // Prevent double slashes 111 | } 112 | path = mergeStringsSingleSlash(path, pathEntry) 113 | } 114 | } 115 | return path; 116 | } else { 117 | return url; 118 | } 119 | } 120 | 121 | export function getAndNormalizeURL(url?: string, shell?: SolidShell) : string { 122 | if (url) { 123 | return normalizeURL(url, shell) 124 | } else if (!url && shell && shell.workingContainer) { 125 | return shell.workingContainer; 126 | } else { 127 | throw new Error('Could not find current working directory') 128 | } 129 | } 130 | 131 | 132 | 133 | 134 | export function getResourceInfoRelativePath(info: any) { return info.relativePath ? info.relativePath : info.url } -------------------------------------------------------------------------------- /src/utils/userInteractions.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | export async function requestUserCLIConfirmationDefaultNegative(request: string) : Promise { 4 | console.log(`${request} [y/N]`); 5 | return await new Promise((resolve, reject) => { 6 | process.stdin.setRawMode(true); 7 | process.stdin.resume(); 8 | process.stdin.once('data', (chk) => { 9 | process.stdin.pause(); 10 | if (chk.toString('utf8') === "y") { 11 | resolve(true); 12 | } else { 13 | resolve(false); 14 | } 15 | }); 16 | }); 17 | } 18 | 19 | export async function requestUserCLIConfirmationDefaultPositive(request: string) : Promise { 20 | console.log(`${request} [Y/n]`); 21 | return await new Promise((resolve, reject) => { 22 | process.stdin.setRawMode(true); 23 | process.stdin.resume(); 24 | process.stdin.once('data', (chk) => { 25 | process.stdin.pause(); 26 | if (chk.toString('utf8') === "n") { 27 | resolve(false); 28 | } else { 29 | resolve(true); 30 | } 31 | }); 32 | }); 33 | } 34 | 35 | export async function requestUserIdp() { 36 | console.log(``); 37 | 38 | let answers = await inquirer.prompt([{ 39 | type: 'input', 40 | name: 'idp', 41 | message: `Could not discover OIDC issuer\nPlease provide OIDC issuer:`}]) 42 | return answers.idp; 43 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bashlib", 3 | "hideGenerator": true, 4 | "entryPoints": "src", 5 | "includeVersion": true, 6 | "out": "docs" 7 | } 8 | --------------------------------------------------------------------------------