├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG ├── CONTRIBUTING ├── LICENSE ├── README.md ├── build.sh ├── config.json-local-nss-compat ├── jest.config.js ├── package-lock.json ├── package.json ├── sample-keystore.json ├── src ├── __mocks__ │ └── node-fetch.ts ├── provision.ts ├── rootRender.ts ├── server.ts ├── standalone.ts ├── streams.ts └── views │ └── home.ejs ├── static ├── index.html └── popup.html ├── templates └── profile.ttl ├── test ├── fixtures │ ├── bearerToken │ └── web │ │ ├── jackson.solid.community │ │ └── 443 │ │ │ ├── .well-known │ │ │ └── openid-configuration │ │ │ └── profile │ │ │ └── card │ │ └── solid.community │ │ └── 443 │ │ └── jwks ├── surface │ ├── change-events.test.ts │ └── helper.ts └── unit │ └── server.test.ts ├── tsconfig.json ├── tslint.json └── types ├── buffer-to-stream.d.ts └── oidc-provider.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # TypeScript build result 9 | dist 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Output from https://github.com/w3c/ldp-testsuite 24 | report 25 | test-output 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | config.json 67 | data 68 | 69 | # next.js build output 70 | .next 71 | 72 | # IDEs 73 | .idea 74 | .vscode 75 | 76 | 77 | .db 78 | data -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | docs 3 | coverage 4 | report 5 | .vscode 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: 5 | - npm run test 6 | - npm run coveralls -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.1.1: 2 | * fix outdated dependencies 3 | * fix copy-paste error in readme 4 | 5 | v0.1.0: 6 | * initial version 7 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Contributions welcome! 2 | 3 | Please open a github issue before you start working on something, so we can try to help you and coordinate. 4 | 5 | When you submit a pull request or contribute to this project in any other way, you commit to your contributions 6 | being included in pod-server by inrupt and made available under the MIT license. 7 | 8 | If your contributions are accepted, and unless you indicate you prefer otherwise, your name will also be added 9 | to the list of contributors in the readme and in the npm package file. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 inrupt, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⛔ Deprecated 2 | This repository is no longer maintained by Inrupt. Of course you can clone the repository if you wish to work in the code yourself. 3 | There are maintained open source Solid servers such as [CSS](https://github.com/solid/community-server) which are still supported. 4 | # pod-server 5 | 6 | [![Build Status](https://travis-ci.org/inrupt/pod-server.svg?branch=master)](https://travis-ci.org/inrupt/pod-server) 7 | 8 | [![Coverage Status](https://coveralls.io/repos/github/inrupt/pod-server/badge.svg?branch=master)](https://coveralls.io/github/inrupt/pod-server?branch=master) 9 | 10 | [![Greenkeeper badge](https://badges.greenkeeper.io/inrupt/pod-server.svg)](https://greenkeeper.io/) 11 | 12 | Solid server package that bind together 13 | [solid-idp](https://github.com/inrupt/solid-idp), 14 | [wac-ldp](https://github.com/inrupt/wac-ldp), 15 | [websockets-pubsub](https://github.com/inrupt/websockets-pubsub), and the 16 | [data-browser](https://github.com/linkeddata/mashlib). 17 | 18 | # Running on localhost, NSS compat mode 19 | 20 | ```sh 21 | git clone https://github.com/inrupt/pod-server 22 | cd pod-server 23 | git checkout dev 24 | npm install 25 | cp config.json-local-nss-compat config.json 26 | cp -r ../../solid/node-solid-server/.db . 27 | cp -r ../../solid/node-solid-server/data . 28 | npm run build 29 | DEBUG=* npm start 30 | ``` 31 | 32 | # Architecture 33 | 34 | ## The Solid Spec Protocols 35 | 36 | This server implements version 0.7 of the [Solid spec](https://github.com/solid/solid-spec). This is a diagram of the layering of protocols: 37 | 38 | ![protocol layers](https://user-images.githubusercontent.com/408412/57321843-78149980-7102-11e9-8c32-4ebda462335e.png) 39 | 40 | ## Functions of the server 41 | Regardless of the layering of protocols, we have a layering of functional components in the software, which looks as follows: 42 | 43 | ![Functional components of inrupt's pod-server](https://user-images.githubusercontent.com/408412/57322032-de99b780-7102-11e9-8a20-9e49e0d44f04.png) 44 | 45 | It shows how the different functional units of the server (persistence, auth, data interface, etc.) depend on each other. 46 | 47 | ## Code modules 48 | This server delegates some of its functions to npm modules it depends on. 49 | 50 | ### solid-idp 51 | The [solid-idp](https://github.com/inrupt/solid-idp) module implements the webid-oidc functionality. Note that a pod-server makes no explicit distinction between local and remote users. In terms of organization, each user gets an identity and a storage space, and that identity is the first to be granted full access to the empty storage space upon its creation. But in technical terms the IDP and the storage are almost entirely separate. The only two connections between them is that the IDP proves control of a profile URL (web id) that points to a document on the storage space, and that the IDP's authorize dialog will edit the list of trusted apps in that profile whenever the user authorizes a new third-party web app. 52 | 53 | The IDP exposes a koa handler to the pod-server. Apart from that, it sends out emails for verifications and password reminders. It also exposes an event when an account is created or deleted, and its dialogs will do xhr edits to trustedApps. 54 | 55 | ### wac-ldp 56 | The [wac-ldp](https://github.com/inrupt/wac-ldp) module is the central part of the pod-server. It exposes a koa handler, which pod-server consumes. 57 | It also emits change events, which the pod-server used to know when to call the `Hub#publish` method from the websocket-pubsub module (see below). 58 | Apart from that, it exposes a number of interfaces which the websockets-pubsub module consumes: 59 | * a function to check [updates-via tickets](https://github.com/inrupt/websockets-pubsub/issues/2#issuecomment-489319630) 60 | * a function for checking whether a given webId has read access to a given resource 61 | 62 | ### websockets-pubsub 63 | The [websockets-pubsub](https://github.com/inrupt/websockets-pubsub) module exposes a 'Hub' object, with a websocket-onconnection handler and a publish method 64 | 65 | ### html statics 66 | Although some pod providers may choose to replace `static/index.html` with the [data-browser](https://github.com/linkeddata/mashlib). This is the content which the pod-server serves when a user visits the pod-server with their browser. 67 | 68 | Published under an MIT license by inrupt, Inc. 69 | 70 | Contributors: 71 | * Michiel de Jong 72 | * Jackson Morgan 73 | * Ruben Verborgh 74 | * Kjetil Kjernsmo 75 | * Pat McBennett 76 | * Justin Bingham 77 | * Sebastien Dubois 78 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | rm -r node_modules 2 | rm package-lock.json 3 | npm install 4 | cd node_modules/solid-idp 5 | npm install 6 | npm run build 7 | cd ../../ 8 | npm run build 9 | -------------------------------------------------------------------------------- /config.json-local-nss-compat: -------------------------------------------------------------------------------- 1 | { 2 | "storage": { 3 | "type": "filesystem", 4 | "rootFolder": "./data" 5 | }, 6 | "network": { 7 | "host": "localhost:8080", 8 | "port": 8080, 9 | "useHttps": false 10 | }, 11 | "htmlRenderer": "mashlib", 12 | "allowProvisioningExternalIdentites": false, 13 | "routing": { 14 | "routingStrategy": "subdomain", 15 | "pathPrefix": "" 16 | }, 17 | "allowExternalDomains": false, 18 | "identityProvider": { 19 | "enabled": true, 20 | "storage": { 21 | "type": "filesystem", 22 | "rootFolder": "./.db" 23 | }, 24 | "keystore": "./sample-keystore.json" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | "/src", 4 | "/test", 5 | ], 6 | preset: 'ts-jest', 7 | testEnvironment: 'node', 8 | verbose: false, 9 | collectCoverage: false, 10 | globals: { 11 | 'ts-jest': { 12 | // reference: https://kulshekhar.github.io/ts-jest/user/config/ 13 | } 14 | }, 15 | transformIgnorePatterns: [ 16 | 'node_modules/(?!(solid-server-ts|.*/node_modules/solid-server-ts)/)' 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pod-server", 3 | "version": "0.3.4", 4 | "description": "Solid pod server, composed from various independent TypeScript modules.", 5 | "main": "dist/app.js", 6 | "dependencies": { 7 | "@types/debug": "^4.1.4", 8 | "@types/koa": "^2.0.49", 9 | "@types/koa-session": "^5.10.1", 10 | "@types/koa-static-server": "^1.3.0", 11 | "@types/koa-views": "^2.0.3", 12 | "@types/node": "^11.12.1", 13 | "@types/redis": "^2.8.12", 14 | "@types/semver": "^6.0.2", 15 | "@types/ws": "^6.0.1", 16 | "buffer-to-stream": "^1.0.0", 17 | "debug": "^4.1.1", 18 | "koa": "^2.7.0", 19 | "koa-session": "^5.12.2", 20 | "koa-static-server": "^1.4.0", 21 | "mashlib": "^1.0.0", 22 | "redis": "^2.8.0", 23 | "semver": "^6.3.0", 24 | "solid-idp": "0.1.5-build3", 25 | "solid-server-ts": "^0.1.1", 26 | "wac-ldp": "^0.9.5", 27 | "websockets-pubsub": "^0.3.7", 28 | "ws": "^6.2.1" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^24.0.11", 32 | "@types/mockdate": "^2.0.0", 33 | "copyfiles": "^2.1.1", 34 | "coveralls": "^3.0.3", 35 | "jest": "^24.7.1", 36 | "mockdate": "^2.0.2", 37 | "ts-jest": "^24.0.2", 38 | "ts-node-dev": "^1.0.0-pre.32", 39 | "tslint": "^5.15.0", 40 | "tslint-config-standard": "^8.0.1", 41 | "typedoc": "^0.14.2", 42 | "typescript": "^3.4.2" 43 | }, 44 | "scripts": { 45 | "start": "node dist/standalone.js", 46 | "dev": "ts-node-dev --project \"tsconfig.json\" src/standalone.ts", 47 | "build": "tsc -p tsconfig.json && npm run copy-template", 48 | "copy-template": "copyfiles -u 1 ./**/*.ejs ./dist && copyfiles -u 1 ./**/*.css ./dist", 49 | "jest": "jest", 50 | "lint": "tslint -t stylish --project \"tsconfig.json\"", 51 | "test": "npm run lint && npm run jest", 52 | "docs": "typedoc --out docs --mode modules src", 53 | "coverage": "jest --coverage", 54 | "coveralls": "npm run coverage && cat ./coverage/lcov.info | coveralls", 55 | "prepare": "cd node_modules/wac-ldp ; npm install ; npm run build ; cd ../websockets-pubsub ; npm install ; npm run build ; cd ../.." 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/inrupt/pod-server.git" 60 | }, 61 | "engines": { 62 | "node": ">=12" 63 | }, 64 | "author": "inrupt, Inc.", 65 | "contributors": [ 66 | "Michiel de Jong (https://github.com/michielbdejong)", 67 | "Jackson Morgan (https://github.com/jaxoncreed)", 68 | "Ruben Verborgh (https://github.com/rubenverborgh)", 69 | "Kjetil Kjernsmo (https://github.com/kjetilk)", 70 | "Pat McBennett (https://github.com/pmcb55)", 71 | "Justin Bingham (https://github.com/justinwb)" 72 | ], 73 | "license": "MIT", 74 | "bugs": { 75 | "url": "https://github.com/inrupt/pod-server/issues" 76 | }, 77 | "homepage": "https://github.com/inrupt/pod-server#readme" 78 | } 79 | -------------------------------------------------------------------------------- /sample-keystore.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "kty": "RSA", 6 | "kid": "xeOjes9u3AcU4LBzcanEM7pZLwSlxaN7U62ZzOBDQuw", 7 | "e": "AQAB", 8 | "n": "oB2LgkiZZ5iLAz1d4ua7sVxdbzY2nIRkDtf4UE08mWsD6UYRzLR98_gMAfnKB8i9yPCQkxfA5w_SZq6Y7odG1qSwLHM2mb_O2GSvY9kaG00UpeeEJCR19c7Jkcmq3GXh4yujnm2TFQ6YAzYNgrXkHlusaFUApJaQN6zr4AvmR_vX_5i__Ku7nuU-GbaV75LSr8o0QANdYFF0ooz5DJvydPplF8mO9_oD7ceSNLWP1AXlFs5JH6MEhH02dELb4-zeLcVzhoqON60cABTpbYSf1lLbYZsVUQ3cYE9CxXaByY2YNuQgc0k29mSmUvwEs0hNA5xUcE3-y_qKpYKniErb9Q", 9 | "d": "FmiMIcuvTIRY0DdCcIMCOaxHl0zrD7SnnDw1kGd-16nWfktEKnYIOqC4bX5b_ALoLLseQLfOU4gvVheRZ7CfBWM_FLl7JsFlXXuZ4Et-D9wVy7I_GB_SMniiVTj4JKhNmNF-sKl9MDE-rRRfh6-VIXqLAn8C_AXmYSReTpjbva8T-fq6vHgB9GmRqW4yRpFta3CA2uJpfnQBzIXNuBHFnk7C7e3omgplXHicuuT3GQnKZlhsREXN1BK7_WcK6OZERqnx-fOl42Sq2pNSSLaLu42vhmvvEXBbHUFkEOU4x0QmpdjhpynQS5yS30xJf9NI9DROTSncVbswjLiI_XPOgQ", 10 | "p": "7mtMx8m9zSzhWezMirL0neazpIw2lBYJStmCkyoT0rKAw0TJ1rx-sLh_Skn6BbTSNoJWZsiMC9AzUaER3LDBQda_LTYAiEtr3q3TeWjs2O7Q_QCGP2CGCpWrYddWKumv6ye2ZdgORlXAuOqO2GavqZJgpo9b9mTfqRq2pPKADfk", 11 | "q": "q-wVzVmX5dZI8O5JLEMaBRbZtQIx0EyyN9Zy8itWgcfvYdU1WY8-KSZg67ZvkOBSScLx8y3V0wcc5kXD6W5PFKqVfwhfKABHimB8QAKPZCb-RBWDbvciNTi1CPJVNkLBtiiI9MWO6VSytOtokskOvHcA4mwMrIfxD_0XGU4YLN0", 12 | "dp": "B9Uck5-sDZaA3Lxrx86zPJC8rBYzINBMg9n7cSw7tHtKwZ975gMRQmr9O4qMnS1gjovfnMbP2v9_ABqDhLWF08zjQO_6OoAHziv1u5JX3ZSS5wziXCimnqhmFfPGD-jXb6lBU70yUts0Vp7WDIPrF24IoNAq3EBaHKsU_vw8erk", 13 | "dq": "ICtZ3QXhtWEGXwbHbF_V85PWAte5SHfBdU9MTOItGrW1pkHF7M8v23VR92k4sQw4eZLfwRgXhZg0ISZ2xSwd4gkVViLT42FCAbOSLEwOVrgxJb48zLuzi-_jeBwYM8IECzjEf8CjwCdYFSBjfevfNQazhKqhKHt7cPlzpAmH3oU", 14 | "qi": "E3W6OfqeAzVCvylUpavcaBQhBRMEvnargBUSD8LT0smIldDm0SuTZ2fzueTfynd-9tvb9Ny_Tr9uSS-yWDHulnPfQL7LOdI1TWAXEy4278FJZwGCElSvjJafK_KS36sTw614YOV0UitWCd21aMWkKlJboh3kzEZEehFNAz2Iq-M", 15 | "key_ops": [ "verify" ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/__mocks__/node-fetch.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { Response, default as realFetch } from 'node-fetch' 3 | import { URL } from 'url' 4 | import Debug from 'debug' 5 | 6 | const debug = Debug('fetch-mock') 7 | 8 | const WEB_FIXTURES = './test/fixtures/web' 9 | 10 | // We want to mock `fetch` but not `Response`, 11 | // so passing that through as-is from the real node-fetch module: 12 | export interface Response extends Response {} 13 | 14 | export default function fetch (urlStr: string, options: any): Promise { 15 | debug('fetch', urlStr) 16 | const url = new URL(urlStr) 17 | const response = fs 18 | return new Promise((resolve, reject) => { 19 | debug('reading web fixture', `${WEB_FIXTURES}/${url.hostname}/${url.port || 443}${url.pathname}`) 20 | fs.readFile(`${WEB_FIXTURES}/${url.hostname}/${url.port || 443}${url.pathname}`, (err, data) => { 21 | if (err) { 22 | debug('error reading web fixture, falling back to real fetch', url) 23 | resolve(realFetch(urlStr, options)) 24 | } else { 25 | debug('success reading web fixture', url) 26 | let streamed = false 27 | let endHandler: any = null 28 | resolve({ 29 | json () { 30 | return JSON.parse(data.toString()) 31 | }, 32 | headers: { 33 | get (name: string) { 34 | if (name === 'content-type') { 35 | return 'text/turtle' 36 | } 37 | } 38 | }, 39 | on (eventType: string, eventHandler: (buf: Buffer) => {}) { 40 | if (eventType === 'end') { 41 | endHandler = eventHandler 42 | } 43 | if (eventType === 'data') { 44 | eventHandler(data) 45 | streamed = true 46 | } 47 | if (streamed && endHandler) { 48 | endHandler() 49 | } 50 | } 51 | } as unknown as Response) 52 | } 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/provision.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import Debug from 'debug' 3 | import { WacLdp } from 'wac-ldp' 4 | import Handlebars from 'handlebars' 5 | 6 | const debug = Debug('provision') 7 | const PROFILE_TURTLE = fs.readFileSync('./templates/profile.ttl') 8 | const profileTurtleTemplate = Handlebars.compile(PROFILE_TURTLE.toString()) 9 | 10 | export async function provisionProfile (wacLdp: WacLdp, webId: URL, screenName: string) { 11 | const profileUrl = new URL(webId.pathname, webId.origin) 12 | debug('provisioning profile', screenName, webId.toString(), profileUrl.toString()) 13 | const turtleDoc: string = profileTurtleTemplate({ 14 | name: screenName, 15 | webId: webId.toString() 16 | }) 17 | await wacLdp.createLocalDocument(profileUrl, 'text/turtle', turtleDoc) 18 | } 19 | 20 | export async function provisionStorage (wacLdp: WacLdp, storageRoot: URL, owner: URL) { 21 | let storageRootStr = storageRoot.toString() 22 | if (storageRootStr.substr(-1) !== '/') { 23 | storageRootStr += '/' 24 | } 25 | debug('provisioning storage', storageRoot.toString(), owner.toString()) 26 | await wacLdp.setRootAcl(storageRoot, owner) 27 | await wacLdp.setPublicAcl(new URL(storageRootStr + 'inbox/'), owner, 'Append') 28 | await wacLdp.setPublicAcl(new URL(storageRootStr + 'public/'), owner, 'Read') 29 | await wacLdp.setPublicAcl(new URL(storageRootStr + 'profile/'), owner, 'Read') 30 | } 31 | -------------------------------------------------------------------------------- /src/rootRender.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import Router from 'koa-router' 3 | import views from 'koa-views' 4 | 5 | export default function getRootRenderRouter (rootOrigin: any): Router { 6 | const router = new Router() 7 | router.all('/', views(path.join(__dirname, 'views'), { extension: 'ejs' })) 8 | router.use(async (ctx, next) => { 9 | if (ctx.path === '/' && ctx.origin === rootOrigin) { 10 | return ctx.render('home', { 11 | rootOrigin 12 | }) 13 | } else { 14 | await next() 15 | } 16 | }) 17 | return router 18 | } 19 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http' 2 | import * as https from 'https' 3 | import * as fs from 'fs' 4 | import Debug from 'debug' 5 | import path from 'path' 6 | import { BlobTree, WacLdp, WacLdpOptions, StoreManager, QuadAndBlobStore, NssCompatResourceStore, DefaultOperationFactory, AclBasedAuthorizer } from 'wac-ldp' 7 | import * as WebSocket from 'ws' 8 | import { Hub } from 'websockets-pubsub' 9 | import Koa from 'koa' 10 | import session from 'koa-session' 11 | import Router from 'koa-router' 12 | import { provisionProfile, provisionStorage } from './provision' 13 | import { defaultConfiguration } from 'solid-idp' 14 | import getRootRenderRouter from './rootRender' 15 | // import { default as Accepts } from 'accepts' 16 | 17 | import { IResourceStore, IOperationFactory, IAuthorizer, IHttpHandler } from 'solid-server-ts' 18 | 19 | const debug = Debug('server') 20 | 21 | const LOGIN_HTML = fs.readFileSync('./static/popup.html') 22 | const DATA_BROWSER_HTML = fs.readFileSync(path.join(__dirname, '../node_modules/mashlib/dist/index.html')) 23 | const DATA_BROWSER_CSS = fs.readFileSync(path.join(__dirname, '../node_modules/mashlib/dist/mash.css')) 24 | const DATA_BROWSER_JS = fs.readFileSync(path.join(__dirname, '../node_modules/mashlib/dist/mashlib.min.js')) 25 | 26 | interface HttpsConfig { 27 | key: Buffer 28 | cert: Buffer 29 | } 30 | 31 | interface OptionsObject { 32 | port: number 33 | rootDomain: string 34 | httpsConfig?: HttpsConfig 35 | storage: BlobTree 36 | keystore: any, 37 | useHttps: boolean 38 | mailConfiguration: any 39 | idpStorage: any 40 | } 41 | 42 | export class Server { 43 | operationFactory: IOperationFactory 44 | authorizer: IAuthorizer 45 | server: http.Server | undefined 46 | hub: Hub | undefined 47 | port: number 48 | lowLevelResourceStore: IResourceStore 49 | midLevelResourceStore: IResourceStore 50 | highLevelResourceStore: IResourceStore 51 | wsServer: any 52 | app: Koa | undefined 53 | idpRouter: any 54 | rootDomain: string 55 | rootOrigin: string 56 | wacLdp: IHttpHandler 57 | httpsConfig: HttpsConfig | undefined 58 | useHttps: boolean 59 | keystore: any 60 | mailConfiguration: any 61 | idpStorage: any 62 | constructor (options: OptionsObject) { 63 | this.port = options.port 64 | this.rootDomain = options.rootDomain 65 | this.httpsConfig = options.httpsConfig 66 | this.useHttps = options.useHttps 67 | this.keystore = options.keystore 68 | this.rootOrigin = `http${(this.useHttps ? 's' : '')}://${this.rootDomain}` 69 | this.lowLevelResourceStore = options.storage 70 | this.midLevelResourceStore = new QuadAndBlobStore(this.lowLevelResourceStore as BlobTree) // singleton on-disk storage 71 | this.highLevelResourceStore = new StoreManager(options.rootDomain, this.midLevelResourceStore as QuadAndBlobStore) 72 | this.operationFactory = new DefaultOperationFactory(this.highLevelResourceStore as StoreManager) 73 | this.authorizer = new AclBasedAuthorizer(this.highLevelResourceStore as StoreManager) 74 | 75 | this.wacLdp = new WacLdp(this.operationFactory, this.authorizer, { 76 | storage: this.midLevelResourceStore as QuadAndBlobStore, 77 | aud: this.rootOrigin, 78 | updatesViaUrl: this.webSocketUrl(), 79 | skipWac: false, 80 | idpHost: options.rootDomain, 81 | usesHttps: true 82 | } as WacLdpOptions) 83 | this.mailConfiguration = options.mailConfiguration 84 | this.idpStorage = options.idpStorage 85 | } 86 | webSocketUrl () { 87 | return new URL(`ws${(this.useHttps ? 's' : '')}://${this.rootDomain}`) 88 | } 89 | storageRootStrToWebIdStr (storageRoot: string) { 90 | return storageRoot + (storageRoot.substr(-1) === '/' ? '' : '/') + 'profile/card#me' 91 | } 92 | screenNameToStorageRootStr (screenName: string) { 93 | // no need to append portSuffix here since it's already part of this.rootDomain 94 | // const defaultPort: Number = (this.useHttps ? 443 : 80) 95 | // const portIsDefault: boolean = (this.port === defaultPort) 96 | // const portSuffix: string = (portIsDefault ? '' : `:${this.port}`) 97 | // return `http${(this.useHttps ? 's' : '')}://${screenName}.${this.rootDomain}${portSuffix}` 98 | return `http${(this.useHttps ? 's' : '')}://${screenName}.${this.rootDomain}` 99 | } 100 | async listen () { 101 | debug('setting IDP issuer to', this.rootOrigin) 102 | this.idpRouter = await defaultConfiguration({ 103 | issuer: this.rootOrigin, 104 | pathPrefix: '', 105 | mailConfiguration: this.mailConfiguration, 106 | webIdFromUsername: async screenname => this.storageRootStrToWebIdStr(this.screenNameToStorageRootStr(screenname)), 107 | onNewUser: async (screenName: string) => { 108 | debug('new user', screenName) 109 | const storageRootStr = this.screenNameToStorageRootStr(screenName) 110 | const webIdStr = this.storageRootStrToWebIdStr(storageRootStr) 111 | await provisionStorage(this.wacLdp as WacLdp, new URL(storageRootStr), new URL(webIdStr)) 112 | await provisionProfile(this.wacLdp as WacLdp, new URL(webIdStr), screenName) 113 | return webIdStr 114 | }, 115 | keystore: this.keystore, 116 | storagePreset: 'filesystem', 117 | storageData: { 118 | folder: this.idpStorage.rootFolder 119 | } 120 | }) 121 | this.app = new Koa() 122 | this.app.proxy = true 123 | this.app.keys = [ 'REPLACE_THIS_LATER' ] 124 | this.app.use(session(this.app)) 125 | this.app.use(async (ctx, next) => { 126 | ctx.req.headers['x-forwarded-proto'] = `http${this.useHttps ? 's' : ''}` 127 | await next() 128 | }) 129 | 130 | const rootRenderRouter = getRootRenderRouter(this.rootOrigin) 131 | this.app.use(rootRenderRouter.routes()) 132 | this.app.use(rootRenderRouter.allowedMethods()) 133 | 134 | // TODO: this way of handling the if statement is ugly 135 | this.app.use(async (ctx, next) => { 136 | if (ctx.origin === this.rootOrigin) { 137 | await this.idpRouter.routes()(ctx, async () => { 138 | await this.idpRouter.allowedMethods()(ctx, next) 139 | }) 140 | } else { 141 | await next() 142 | } 143 | }) 144 | 145 | // HACK: in order for the login page to show up, a separate file must be run at /.well-known/solid/login which I find very dirty -- jackson 146 | const loginRouter = new Router() 147 | loginRouter.get('/.well-known/openid-configuration', (ctx, next) => { 148 | ctx.status = 301 149 | ctx.res.setHeader('Access-Control-Allow-Origin', '*') 150 | ctx.redirect(`${this.rootOrigin}/.well-known/openid-configuration`) 151 | ctx.body = 'Redirecting to openid configuration' 152 | }) 153 | loginRouter.get('/.well-known/solid/login', (ctx, next) => { 154 | debug('sending login html') 155 | ctx.res.writeHead(200, {}) 156 | ctx.res.end(LOGIN_HTML) 157 | ctx.respond = false 158 | }) 159 | this.app.use(loginRouter.routes()) 160 | this.app.use(loginRouter.allowedMethods()) 161 | 162 | // Data Browser 163 | const dataBrowserFilesRouter = new Router() 164 | dataBrowserFilesRouter.get('/mash.css', (ctx, next) => { 165 | ctx.res.writeHead(200, {}) 166 | ctx.res.end(DATA_BROWSER_CSS) 167 | }) 168 | dataBrowserFilesRouter.get('/mashlib.min.js', (ctx, next) => { 169 | ctx.res.writeHead(200, {}) 170 | ctx.res.end(DATA_BROWSER_JS) 171 | }) 172 | this.app.use(dataBrowserFilesRouter.routes()) 173 | this.app.use(dataBrowserFilesRouter.allowedMethods()) 174 | this.app.use(async (ctx, next) => { 175 | if (ctx.accepts(['text/turtle', 'application/ld+json', 'html']) === 'html') { 176 | debug('redirect to data browser!') 177 | ctx.res.writeHead(200, {}) 178 | ctx.res.end(DATA_BROWSER_HTML) 179 | } else { 180 | debug('skipping data browser') 181 | await next() 182 | } 183 | }) 184 | // END HACK 185 | 186 | this.app.use(async (ctx, next) => { 187 | debug('LDP handler', ctx.req.method, ctx.req.url) 188 | await this.wacLdp.handle(ctx.req, ctx.res) 189 | ctx.respond = false 190 | }) 191 | if (this.httpsConfig) { 192 | this.server = https.createServer(this.httpsConfig, this.app.callback()) 193 | } else { 194 | this.server = http.createServer(this.app.callback()) 195 | } 196 | this.server.listen(this.port) 197 | this.wsServer = new WebSocket.Server({ 198 | server: this.server 199 | }) 200 | this.hub = new Hub(this.wacLdp as WacLdp, this.rootOrigin) 201 | this.wsServer.on('connection', this.hub.handleConnection.bind(this.hub)) 202 | ;(this.wacLdp as WacLdp).on('change', (event: { url: URL }) => { 203 | if (this.hub) { 204 | this.hub.publishChange(event.url) 205 | } 206 | }) 207 | debug('listening on port', this.port, (this.httpsConfig ? 'https' : 'http')) 208 | } 209 | close () { 210 | if (this.server) { 211 | this.server.close() 212 | } 213 | if (this.wsServer) { 214 | this.wsServer.close() 215 | } 216 | debug('closing port', this.port) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/standalone.ts: -------------------------------------------------------------------------------- 1 | import { Server } from './server' 2 | import * as fs from 'fs' 3 | import { BlobTreeNssCompat, BlobTree } from 'wac-ldp' 4 | import Debug from 'debug' 5 | import semver from 'semver' 6 | import { engines } from '../package.json' 7 | 8 | const debug = Debug('standalone') 9 | 10 | const config = JSON.parse( 11 | (process.env.CONFIG_PATH ? fs.readFileSync(process.env.CONFIG_PATH) : fs.readFileSync('./config.json')).toString() 12 | ) 13 | 14 | console.log(config) 15 | 16 | const version = engines.node 17 | if (!semver.satisfies(process.version, version)) { 18 | console.log(`Required node version ${version} not satisfied with current version ${process.version}.`) 19 | process.exit(1) 20 | } 21 | console.log(`Checked that Node version ${process.version} satisfied ${version}.`) 22 | 23 | // on startup: 24 | const port = parseInt((config.network.port ? config.network.port : ''), 10) || 8080 25 | 26 | const rootDomain = config.network.host || `localhost:${port}` 27 | 28 | const skipWac: boolean = !!process.env.SKIP_WAC 29 | 30 | const tlsDir = process.env.TLS_DIR 31 | let httpsConfig 32 | if (config.network.ssl) { 33 | httpsConfig = { 34 | key: fs.readFileSync(config.network.ssl.key), 35 | cert: fs.readFileSync(config.network.ssl.cert) 36 | } 37 | } 38 | const useHttps = config.network.useHttps || !!config.network.ssl 39 | 40 | // TODO: come back to allow configs to have multi user mode 41 | // let ownerStr: string | undefined = process.env.OWNER 42 | // if (!ownerStr) { 43 | // // throw new Error('OWNER environment variable required') 44 | // ownerStr = 'https://jackson.solid.community/profile/card#me' 45 | // } 46 | 47 | let storage: BlobTree 48 | storage = new BlobTreeNssCompat(config.storage.rootFolder || './data-dir/') 49 | 50 | let keystore: any 51 | if (config.identityProvider.keystore) { 52 | try { 53 | keystore = JSON.parse(fs.readFileSync(config.identityProvider.keystore).toString()) 54 | } catch (e) { 55 | console.error('failed to read IDP keystore from ', config.identityProvider.keystore) 56 | } 57 | } 58 | 59 | const server = new Server({ 60 | port, 61 | rootDomain, 62 | httpsConfig, 63 | storage, 64 | keystore, 65 | useHttps, 66 | mailConfiguration: config.identityProvider.email, 67 | idpStorage: config.identityProvider.storage 68 | }) 69 | 70 | async function startServer () { 71 | // await server.provision() 72 | await server.listen() 73 | console.log('listening on ' + port) 74 | } 75 | 76 | // tslint:disable-next-line: no-floating-promises 77 | startServer() 78 | 79 | // server.close() 80 | -------------------------------------------------------------------------------- /src/streams.ts: -------------------------------------------------------------------------------- 1 | import convert from 'buffer-to-stream' 2 | 3 | export function bufferToStream (buffer: Buffer): any { 4 | return convert(buffer) 5 | } 6 | 7 | export async function streamToBuffer (stream: any): Promise { 8 | const bufs: Array = [] 9 | return new Promise(resolve => { 10 | stream.on('data', function (d: Buffer) { 11 | bufs.push(d) 12 | }) 13 | stream.on('end', function () { 14 | resolve(Buffer.concat(bufs)) 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Solid 7 | 8 | 9 | 10 | 25 | 26 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Solid Data Browser 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /static/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Select your Identity Provider 5 | 6 | 7 | 8 | 9 |
10 | 26 | 27 | -------------------------------------------------------------------------------- /templates/profile.ttl: -------------------------------------------------------------------------------- 1 | @prefix solid: . 2 | @prefix foaf: . 3 | @prefix pim: . 4 | @prefix schema: . 5 | @prefix ldp: . 6 | 7 | <> 8 | a foaf:PersonalProfileDocument ; 9 | foaf:maker <{{webId}}> ; 10 | foaf:primaryTopic <{{webId}}> . 11 | 12 | <{{webId}}> 13 | a foaf:Person ; 14 | a schema:Person ; 15 | 16 | foaf:name "{{name}}" ; 17 | 18 | solid:account ; # link to the account uri 19 | pim:storage ; # root storage 20 | 21 | ldp:inbox ; 22 | 23 | pim:preferencesFile ; # private settings/preferences 24 | solid:publicTypeIndex ; 25 | solid:privateTypeIndex . 26 | -------------------------------------------------------------------------------- /test/fixtures/bearerToken: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3BoZXl2YWVyLmdpdGh1Yi5pbyIsImF1ZCI6Imh0dHBzOi8vamFja3Nvbi5zb2xpZC5jb21tdW5pdHkiLCJleHAiOjE1NjA3NzM4OTcsImlhdCI6MTU2MDc3MDI5NywiaWRfdG9rZW4iOiJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNkluaGxUMnBsY3psMU0wRmpWVFJNUW5wallXNUZUVGR3V2t4M1UyeDRZVTQzVlRZeVducFBRa1JSZFhjaWZRLmV5SnpkV0lpT2lKb2RIUndjem92TDJwaFkydHpiMjR1YzI5c2FXUXVZMjl0YlhWdWFYUjVMM0J5YjJacGJHVXZZMkZ5WkNOdFpTSXNJbTV2Ym1ObElqb2lXSHB4YVVkVGFqRldTM1p0WWxCblNtbFdOMEZRYUcxYU5VaHlaVk5wWDJOUGJFMUNNbTVwTjJ0MldTSXNJbk5wWkNJNkltRmxPVGszTkRNMUxUSXlZMlF0TkRKbU5DMWhaRFkzTFRaaVl6WTBaRFV5TXpSa1lTSXNJbUYwWDJoaGMyZ2lPaUprUjBKWFF6VnVkRk5RY21aaGR6TXdTV1YwWVc5Uklpd2ljMTlvWVhOb0lqb2llRFJYUlV3eVkzZzFhMGxOZDB0RWRXVnFkRE56ZHlJc0ltTnVaaUk2ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbVVpT2lKQlVVRkNJaXdpWlhoMElqcDBjblZsTENKclpYbGZiM0J6SWpwYkluWmxjbWxtZVNKZExDSnJkSGtpT2lKU1UwRWlMQ0p1SWpvaWVEaERObWRMY0Uxd01saDVOWFJGVmtRNU4yUk9YMGd3VG5KWFZUWldORzlLWmpOellrVnBWRUp3YVc5b1oxVnhTbDkzY1dKNVR6UTRlbVpEZVhOTE5ITTVaVm8zTW1SMWJUWkxNWEJpYkVSRlkxaE1kMTgzUjIxQ2IyZFRWbUp0YUdVNFlsQnBNa1oxU2pOclIwSnZkV05ITkRWcldrdzFNVEJYV0VkcGNsQkpibUl4VUhWSVF6TlJXakpxVkhaQmIwUTVaRWx1T1d0RU16ZzBVR2d4ZDJselJuSk1UVzB4UW1kMmJUTlNaek01VGxCTlNtMVdSR1pmY1drd2IwYzFSRFJEY0dSaVV6QlBkVkUxWm01NVVXdGpSSFpPYlc1R1VVZ3dNRll5TkRSTWNXUnZWakZ0UW00eFZYWnhOaTFJUkVvemIyRmFUMWg1TTBabVFuUm5SWFZ3VkhCSGEzQnRUbVpKY1dWbGJrOUtVekJzWHpnMVJFNTFUSGRTUm1ndFVsbDRRbDlsUWswNU1VbDZkRnAwY1dwTFR6WnJUVzFVZFZadVJucEVNbTFPZEVSelJIVjZNVTVMU0hsSE1rbHpkVzF3UlVGUkluMHNJbUYxWkNJNkltaDBkSEJ6T2k4dmNHaGxlWFpoWlhJdVoybDBhSFZpTG1sdklpd2laWGh3SWpveE5UWXdOemN6T0RreUxDSnBZWFFpT2pFMU5qQTNOekF5T1RJc0ltbHpjeUk2SW1oMGRIQnpPaTh2Ykc5allXeG9iM04wT2pnME5ETWlmUS5ZWFM0MlhlbWFzajZEUk5lNEd5LVp3MDBoLXpFU29YRnE1YVNUR3MwckpiMExTMkdhd2RPa19iSEd0ZVZrMHNLdUtfZFhobHlnUl9DeDNqOHE2QmVSWDFTTHdPSkpyLTc4dkZZMFV1R2ZvRjZMSXJjM1dSaXpCMmtoZEt2WVBhNThXSDk2Um00YlBaNVVZTzUzYnFMYnE0ejRWWFU0b19tLVFvUzdUUEtmQ0tRVWd4ZG9HUkJUWXlnZUxzYmx3dFQzeXRwYXhjYjFBVjFYc19zWFhuelpqMnNCcURoaUs3T0ZjdmNPbi0waUxfTUVNS01VVFhfMHNnSTBsc0ZIbU5wdmtwU3hCT2NOZVVfWDlhWVF4YXp3VXpjdWNJM3lxQ1FYam9Oa0ktYXdsMzJ4VkRfd25IN2FYd19RSTVUOU1JckhGOG9XWXBhRVp6LXE3SG1kRVFVa0EiLCJ0b2tlbl90eXBlIjoicG9wIn0.iLAwGcEi-LmibDum3rMxGpVGz9lXHqeLR9uXImCM097Mm29EeIcZX8Pgb0W3T2jQSKlL0HuiSGO1bkl5sEQqLq4FswXrSARnOjnEQt_uQZRj3Hzm7BWH_MpHKeTzvMXQayeJyqyV6w_gvpAeSYC5Lz4ybESajc8bWtBZ_2O4SQG5L3wFUv_GkYFUL8gTPOWI8F9bpSTz_Q99EftjD0DvJQeEMJTqX5XHECFZvx5PfV36syA82xlLEF_yrLuQqozBnlrAKPDGuPsgxKwjwlipXgO1ToZ_rdL0rwSNcoyRoRHv9_POhdYsAqWhTiGVxHq0xiHqqeJMtdQcl-ZtGx1XRQ -------------------------------------------------------------------------------- /test/fixtures/web/jackson.solid.community/443/.well-known/openid-configuration: -------------------------------------------------------------------------------- 1 | {"issuer":"https://solid.community","authorization_endpoint":"https://solid.community/authorize","token_endpoint":"https://solid.community/token","userinfo_endpoint":"https://solid.community/userinfo","jwks_uri":"https://solid.community/jwks","registration_endpoint":"https://solid.community/register","response_types_supported":["code","code token","code id_token","id_token","id_token token","code id_token token","none"],"response_modes_supported":["query","fragment"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256","RS384","RS512","none"],"token_endpoint_auth_methods_supported":["client_secret_basic"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"display_values_supported":[],"claim_types_supported":["normal"],"claims_supported":[],"claims_parameter_supported":false,"request_parameter_supported":true,"request_uri_parameter_supported":false,"require_request_uri_registration":false,"check_session_iframe":"https://solid.community/session","end_session_endpoint":"https://solid.community/logout"} 2 | -------------------------------------------------------------------------------- /test/fixtures/web/jackson.solid.community/443/profile/card: -------------------------------------------------------------------------------- 1 | 2 | @prefix : . 3 | @prefix acl: . 4 | 5 | :me 6 | acl:trustedApp 7 | [ 8 | acl:mode acl:Append, acl:Read, acl:Write; 9 | acl:origin 10 | ]. 11 | -------------------------------------------------------------------------------- /test/fixtures/web/solid.community/443/jwks: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"RSA","kid":"xeOjes9u3AcU4LBzcanEM7pZLwSlxaN7U62ZzOBDQuw","alg":"RS256","key_ops":["verify"],"e":"AQAB","n":"oB2LgkiZZ5iLAz1d4ua7sVxdbzY2nIRkDtf4UE08mWsD6UYRzLR98_gMAfnKB8i9yPCQkxfA5w_SZq6Y7odG1qSwLHM2mb_O2GSvY9kaG00UpeeEJCR19c7Jkcmq3GXh4yujnm2TFQ6YAzYNgrXkHlusaFUApJaQN6zr4AvmR_vX_5i__Ku7nuU-GbaV75LSr8o0QANdYFF0ooz5DJvydPplF8mO9_oD7ceSNLWP1AXlFs5JH6MEhH02dELb4-zeLcVzhoqON60cABTpbYSf1lLbYZsVUQ3cYE9CxXaByY2YNuQgc0k29mSmUvwEs0hNA5xUcE3-y_qKpYKniErb9Q"}]} -------------------------------------------------------------------------------- /test/surface/change-events.test.ts: -------------------------------------------------------------------------------- 1 | import { startServer, stopServer } from './helper' 2 | import fetch from 'node-fetch' 3 | import WebSocket from 'ws' 4 | import fs from 'fs' 5 | import MockDate from 'mockdate' 6 | 7 | let wsClient: WebSocket 8 | let received: Promise 9 | 10 | const bearerToken = fs.readFileSync('test/fixtures/bearerToken') 11 | 12 | beforeEach(async () => { 13 | MockDate.set(1434319925275) 14 | await startServer(8081) 15 | const options = await fetch('http://localhost:8081/asdf/test.txt', { 16 | method: 'HEAD', 17 | headers: { 18 | authorization: 'Bearer ' + bearerToken 19 | } 20 | }) 21 | const updatesVia = options.headers.get('updates-via') 22 | if (!updatesVia) { 23 | throw new Error('No Updates-Via header found on HEAD') 24 | } 25 | wsClient = new WebSocket(updatesVia, undefined, { origin: 'https://pheyvaer.github.io' }) 26 | received = new Promise((resolve) => { 27 | wsClient.on('message', function incoming (data) { 28 | resolve(data.toString()) 29 | }) 30 | }) 31 | await new Promise((resolve) => { 32 | wsClient.on('open', function open () { 33 | wsClient.send('sub http://localhost:8081/asdf/') 34 | resolve(undefined) 35 | }) 36 | }) 37 | }) 38 | afterEach(() => { 39 | MockDate.reset() 40 | wsClient.close() 41 | stopServer() 42 | }) 43 | 44 | // pending https://github.com/inrupt/wac-ldp/issues/87 45 | test.skip('publishes a change event', async () => { 46 | await fetch('http://localhost:8081/asdf/test.txt', { 47 | method: 'PUT', 48 | body: 'hello', 49 | headers: { 50 | authorization: 'Bearer ' + bearerToken 51 | } 52 | }) 53 | const notif = await received 54 | expect(notif).toEqual('pub http://localhost:8081/asdf/test.txt') 55 | }) 56 | -------------------------------------------------------------------------------- /test/surface/helper.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '../../src/server' 2 | import { BlobTreeInMem, QuadAndBlobStore } from 'wac-ldp' 3 | 4 | let server: Server 5 | 6 | // interface OptionsObject { 7 | // port: number 8 | // rootDomain: string 9 | // httpsConfig?: HttpsConfig 10 | // storage: BlobTree 11 | // keystore: any, 12 | // useHttps: boolean 13 | // mailConfiguration: any 14 | // idpStorage: any 15 | // } 16 | 17 | export function startServer (port: number): Promise { 18 | server = new Server({ 19 | port, 20 | rootDomain: `localhost:${port}`, 21 | storage: new BlobTreeInMem(), 22 | keystore: {}, 23 | useHttps: false, 24 | mailConfiguration: undefined, 25 | idpStorage: { 26 | type: 'filesystem', 27 | rootFolder: '' 28 | } 29 | }) 30 | return server.listen() 31 | } 32 | 33 | export function stopServer () { 34 | server.close() 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/server.test.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '../../src/server' 2 | import { BlobTreeInMem, QuadAndBlobStore } from 'wac-ldp' 3 | 4 | const PORT = 8082 5 | 6 | let server: Server 7 | 8 | // interface OptionsObject { 9 | // port: number 10 | // rootDomain: string 11 | // httpsConfig?: HttpsConfig 12 | // storage: BlobTree 13 | // keystore: any, 14 | // useHttps: boolean 15 | // mailConfiguration: any 16 | // idpStorage: any 17 | // } 18 | 19 | test('server', async () => { 20 | server = new Server({ 21 | port: PORT, 22 | rootDomain: `localhost:${PORT}`, 23 | storage: new BlobTreeInMem(), 24 | keystore: { 25 | "keys": [ 26 | { 27 | "alg": "RS256", 28 | "kty": "RSA", 29 | "kid": "xeOjes9u3AcU4LBzcanEM7pZLwSlxaN7U62ZzOBDQuw", 30 | "e": "AQAB", 31 | "n": "oB2LgkiZZ5iLAz1d4ua7sVxdbzY2nIRkDtf4UE08mWsD6UYRzLR98_gMAfnKB8i9yPCQkxfA5w_SZq6Y7odG1qSwLHM2mb_O2GSvY9kaG00UpeeEJCR19c7Jkcmq3GXh4yujnm2TFQ6YAzYNgrXkHlusaFUApJaQN6zr4AvmR_vX_5i__Ku7nuU-GbaV75LSr8o0QANdYFF0ooz5DJvydPplF8mO9_oD7ceSNLWP1AXlFs5JH6MEhH02dELb4-zeLcVzhoqON60cABTpbYSf1lLbYZsVUQ3cYE9CxXaByY2YNuQgc0k29mSmUvwEs0hNA5xUcE3-y_qKpYKniErb9Q", 32 | "d": "FmiMIcuvTIRY0DdCcIMCOaxHl0zrD7SnnDw1kGd-16nWfktEKnYIOqC4bX5b_ALoLLseQLfOU4gvVheRZ7CfBWM_FLl7JsFlXXuZ4Et-D9wVy7I_GB_SMniiVTj4JKhNmNF-sKl9MDE-rRRfh6-VIXqLAn8C_AXmYSReTpjbva8T-fq6vHgB9GmRqW4yRpFta3CA2uJpfnQBzIXNuBHFnk7C7e3omgplXHicuuT3GQnKZlhsREXN1BK7_WcK6OZERqnx-fOl42Sq2pNSSLaLu42vhmvvEXBbHUFkEOU4x0QmpdjhpynQS5yS30xJf9NI9DROTSncVbswjLiI_XPOgQ", 33 | "p": "7mtMx8m9zSzhWezMirL0neazpIw2lBYJStmCkyoT0rKAw0TJ1rx-sLh_Skn6BbTSNoJWZsiMC9AzUaER3LDBQda_LTYAiEtr3q3TeWjs2O7Q_QCGP2CGCpWrYddWKumv6ye2ZdgORlXAuOqO2GavqZJgpo9b9mTfqRq2pPKADfk", 34 | "q": "q-wVzVmX5dZI8O5JLEMaBRbZtQIx0EyyN9Zy8itWgcfvYdU1WY8-KSZg67ZvkOBSScLx8y3V0wcc5kXD6W5PFKqVfwhfKABHimB8QAKPZCb-RBWDbvciNTi1CPJVNkLBtiiI9MWO6VSytOtokskOvHcA4mwMrIfxD_0XGU4YLN0", 35 | "dp": "B9Uck5-sDZaA3Lxrx86zPJC8rBYzINBMg9n7cSw7tHtKwZ975gMRQmr9O4qMnS1gjovfnMbP2v9_ABqDhLWF08zjQO_6OoAHziv1u5JX3ZSS5wziXCimnqhmFfPGD-jXb6lBU70yUts0Vp7WDIPrF24IoNAq3EBaHKsU_vw8erk", 36 | "dq": "ICtZ3QXhtWEGXwbHbF_V85PWAte5SHfBdU9MTOItGrW1pkHF7M8v23VR92k4sQw4eZLfwRgXhZg0ISZ2xSwd4gkVViLT42FCAbOSLEwOVrgxJb48zLuzi-_jeBwYM8IECzjEf8CjwCdYFSBjfevfNQazhKqhKHt7cPlzpAmH3oU", 37 | "qi": "E3W6OfqeAzVCvylUpavcaBQhBRMEvnargBUSD8LT0smIldDm0SuTZ2fzueTfynd-9tvb9Ny_Tr9uSS-yWDHulnPfQL7LOdI1TWAXEy4278FJZwGCElSvjJafK_KS36sTw614YOV0UitWCd21aMWkKlJboh3kzEZEehFNAz2Iq-M", 38 | "key_ops": [ "verify" ] 39 | } 40 | ] 41 | }, 42 | useHttps: false, 43 | mailConfiguration: undefined, 44 | idpStorage: { 45 | type: 'filesystem', 46 | rootFolder: './.db' 47 | } 48 | }) 49 | await server.listen() 50 | server.close() 51 | }) 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "node", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { "*": ["types/*"] }, 6 | "target": "es2015", 7 | "strict": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "removeComments": true, 11 | "preserveConstEnums": true, 12 | "resolveJsonModule": true, 13 | "sourceMap": true, 14 | "outDir": "dist", 15 | "declaration": true, 16 | "esModuleInterop": true 17 | }, 18 | "typeRoots": ["./types"], 19 | "exclude": [ 20 | "test", 21 | "node_modules", 22 | "dist" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard"], 3 | "defaultSeverity": "error", 4 | "linterOptions": { 5 | "exclude": [ 6 | "node_modules/**/*.ts" 7 | ] 8 | }, 9 | "rules": {} 10 | } 11 | -------------------------------------------------------------------------------- /types/buffer-to-stream.d.ts: -------------------------------------------------------------------------------- 1 | export = index; 2 | declare function index(buf: any): any; 3 | -------------------------------------------------------------------------------- /types/oidc-provider.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'oidc-provider' { 2 | 3 | /** Declaration file generated by dts-gen */ 4 | import { IncomingMessage, ServerResponse } from "http"; 5 | import { Http2ServerRequest, Http2ServerResponse } from "http2"; 6 | import { EventEmitter } from "events"; 7 | import Keygrip from 'keygrip'; 8 | import { callbackPromise } from "nodemailer/lib/shared"; 9 | import { Context } from "koa"; 10 | import { GotOptions } from "got"; 11 | 12 | export = Provider; 13 | 14 | // TODO: this is possibly a bad definition for middleware 15 | type routerMiddleware = (ctx: Context, next: () => Promise) => Promise 16 | 17 | class Provider extends EventEmitter { 18 | proxy: boolean 19 | constructor(issuer: string, config?: Provider.ProviderConfiguration); 20 | cookieName(name: string): string; 21 | interactionDetails(req: IncomingMessage | Http2ServerRequest): Promise; 22 | // TODO: provide a type for the configuration of interaction finished and check to be sur 23 | interactionFinished(req: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse, result: Provider.InteractionResult): Promise; 24 | interactionResult(req: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse, result: Provider.InteractionResult): Promise; 25 | listen(port: number, callback: () => void): void; 26 | // TODO: figure out the type for options 27 | pathFor(name: string, options?: {}): string; 28 | registerGrantType( 29 | grantType: string, 30 | tokenExchangeHandler: routerMiddleware, 31 | parameters: string[], 32 | allowedDuplicateParameters: string[] 33 | ): void; 34 | // TODO: figure out the type for payload (if any) 35 | registerResponseMode(name: string, handler: (req: IncomingMessage | Http2ServerRequest, redirectUri: string, payload: {}) => void): void; 36 | setProviderSession(req: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse, session: { 37 | account: string, 38 | ts: number, 39 | remember: boolean, 40 | clients: string[], 41 | meta: { [clientId: string]: {} } 42 | }): void; 43 | // TODO: figure out the type for options (same as pathFor) 44 | urlFor(name: string, options?: {}): void; 45 | use(...middleware: routerMiddleware[]): void; 46 | callback(eq: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse, next: Promise): Promise 47 | } 48 | 49 | namespace Provider { 50 | 51 | type SubjectType = "public" | "parwise"; 52 | type AuthMethod = | "none" | "client_secret_basic" | "client_secret_post" | "client_secret_jwt" | "private_key_jwt" | "tls_client_auth" | "self_signed_tls_client_auth"; 53 | type EncryptionAlgValue = | "RSA-OAEP" | "RSA1_5" | "ECDH-ES" | "ECDH-ES+A128KW" | "ECDH-ES+A192KW" | "ECDH-ES+A256KW" | "A128KW" | "A192KW" | "A256KW" | "A128GCMKW" | "A192GCMKW" | "A256GCMKW" | "PBES2-HS256+A128KW" | "PBES2-HS384+A192KW" | "PBES2-HS512+A256KW"; 54 | type EncryptionEncValue = | "A128CBC-HS256" | "A128GCM" | "A192CBC-HS384" | "A192GCM" | "A256CBC-HS512" | "A256GCM"; 55 | type SigningAlgValue = | "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" | "ES256" | "ES384" | "ES512"; 56 | type OptionalSigningAlgValue = SigningAlgValue | "none"; 57 | type Use = "sig" | "enc"; 58 | 59 | interface RsaKeyBasic { 60 | kty: 'RSA'; 61 | e: string; 62 | n: string; 63 | use?: Use; 64 | kid?: string; 65 | alg?: string; 66 | } 67 | 68 | interface EcKeyBasic { 69 | kty: 'EC'; 70 | crv: string; 71 | x: string; 72 | y: string; 73 | use?: Use; 74 | kid?: string; 75 | alg?: string; 76 | } 77 | 78 | interface RsaKey extends RsaKeyBasic { 79 | d: string; 80 | dp: string; 81 | dq: string; 82 | p: string; 83 | q: string; 84 | qi: string; 85 | } 86 | 87 | interface EcKey extends EcKeyBasic { 88 | d: string; 89 | } 90 | 91 | // TODO: I don't think this is a comprehensive set of what could e a JWKSet 92 | interface JWKSet { 93 | keys: RsaKeyBasic | EcKeyBasic | RsaKey | EcKey 94 | } 95 | 96 | interface Adapter { 97 | name: string 98 | upsert(id: string, payload: { [key: string]: string }, expiresIn: number): Promise 99 | find(id: string): Promise<{}> 100 | findByUserCode(userCode: string): Promise<{ [key: string]: string }> 101 | findByUid(uid: string): Promise<{ [key: string]: string }> 102 | consume(id: string): Promise 103 | destroy(id: string): Promise 104 | revokeByGrantId(grantId: string): Promise 105 | } 106 | 107 | interface Account { 108 | accountId: string 109 | claims: (use: 'id_token' | 'userinfo', scope: string, claims: Object, rejected: string[]) => Promise<{ sub: string, [key: string]: any }> 110 | } 111 | 112 | interface Client { 113 | application_type?: "web" | "native"; 114 | authorization_encrypted_response_alg?: EncryptionAlgValue; 115 | authorization_encrypted_response_enc?: EncryptionEncValue; 116 | authorization_signed_response_alg?: SigningAlgValue; 117 | backchannel_logout_session_required?: boolean; 118 | backchannel_logout_uri?: string; 119 | client_id: string; 120 | client_name?: string; 121 | client_secret?: string; 122 | default_acr_values?: string[]; 123 | default_max_age?: number; 124 | frontchannel_logout_session_required?: boolean; 125 | frontchannel_logout_uri?: string; 126 | grant_types: string[]; 127 | id_token_encrypted_response_alg?: EncryptionAlgValue; 128 | id_token_encrypted_response_enc?: EncryptionEncValue; 129 | id_token_signed_response_alg?: SigningAlgValue; 130 | initiate_login_uri?: string; 131 | introspection_encrypted_response_alg?: EncryptionAlgValue; 132 | introspection_encrypted_response_enc?: EncryptionEncValue; 133 | introspection_endpoint_auth_method?: AuthMethod; 134 | introspection_endpoint_auth_signing_alg?: SigningAlgValue; 135 | introspection_signed_response_alg?: SigningAlgValue; 136 | jwks?: { 137 | keys: (RsaKeyBasic | EcKeyBasic)[] 138 | }; 139 | jwks_uri?: string; 140 | logo_uri?: string; 141 | policy_uri?: string; 142 | post_logout_redirect_uris?: string[]; 143 | request_object_encryption_alg?: EncryptionAlgValue; 144 | request_object_encryption_enc?: EncryptionEncValue; 145 | request_object_signing_alg?: SigningAlgValue; 146 | redirect_uris: string[]; 147 | request_uris?: string[]; 148 | require_auth_time?: boolean; 149 | response_types: string[]; 150 | revocation_endpoint_auth_method?: AuthMethod; 151 | revocation_endpoint_auth_signing_alg?: SigningAlgValue; 152 | scope: string[] 153 | sector_identifier_uri?: string; 154 | subject_type?: SubjectType; 155 | tls_client_auth_san_dns?: string; 156 | tls_client_auth_san_email?: string; 157 | tls_client_auth_san_ip?: string; 158 | tls_client_auth_san_uri?: string; 159 | tls_client_auth_subject_dn?: string; 160 | tls_client_certificate_bound_access_tokens?: boolean; 161 | token_endpoint_auth_method?: AuthMethod; 162 | token_endpoint_auth_signing_alg?: SigningAlgValue; 163 | tos_uri?: string; 164 | userinfo_encrypted_response_alg?: EncryptionAlgValue; 165 | userinfo_encrypted_response_enc?: EncryptionEncValue; 166 | userinfo_signed_response_alg?: SigningAlgValue; 167 | web_message_uris?: string[]; 168 | } 169 | 170 | interface FeatureConfiguration { 171 | enabled?: boolean, 172 | ack?: number 173 | } 174 | 175 | 176 | interface ProviderConfiguration { 177 | adapter?: new (name: string) => Adapter 178 | clients?: Client[] 179 | findAccount?: (ctx: Context, sub: string, token?: string) => Promise 180 | jwks?: JWKSet 181 | features?: { 182 | backchannelLogout?: FeatureConfiguration 183 | claimsParameter?: FeatureConfiguration 184 | clientCredentials?: FeatureConfiguration 185 | dPoP?: { 186 | enabled?: boolean 187 | ack?: number 188 | iatTolerance?: number 189 | } 190 | devInteractions?: FeatureConfiguration 191 | deviceFlow?: { 192 | charset?: 'base-20' | 'digits' 193 | deviceInfo?: (ctx: Context) => { ip: string, ua: string } 194 | mask?: string 195 | successSource?: (ctx: Context) => Promise 196 | // TODO outline device info parameter 197 | userCodeConfirmSource?: (ctx: Context, form: string, client: Client, deviceInfo: {}, userCode: string) => Promise 198 | userCodeInputSource?: (ctx: Context, form: string, out: { [key: string]: string }, err: Error) => Promise 199 | } 200 | encryption?: FeatureConfiguration 201 | frontchannelLogout?: { 202 | enabled?: boolean 203 | ack?: number 204 | logoutPendingSource?: (ctx: Context, frames: string[], postLogoutRedirectUri: string) => Promise 205 | } 206 | introspection?: FeatureConfiguration 207 | jwtIntrospection?: FeatureConfiguration 208 | jwtResponseModes?: FeatureConfiguration 209 | jwtUserinfo?: FeatureConfiguration 210 | mTLS?: { 211 | enabled?: boolean 212 | certificateAuthorized?: (ctx: Context) => Promise 213 | certificateBoundAccessTokens?: boolean 214 | certificateSubjectMatches?: (ctx: Context, property: string, expected: string) => Promise 215 | getCertificate?: (ctx: Context) => Promise 216 | selfSignedTlsClientAuth?: boolean 217 | tlsClientAuth?: boolean 218 | } 219 | registration?: { 220 | enabled?: boolean 221 | idFactory?: () => Promise 222 | initialAccessToken?: boolean 223 | policies?: { 224 | [key: string]: (ctx: Context, properties: Client) => Promise 225 | } 226 | secretFactory?: () => Promise 227 | } 228 | registrationManagement?: { 229 | enabled?: boolean 230 | rotateRegistrationAccessToken?: (ctx: Context) => Promise 231 | } 232 | request?: FeatureConfiguration 233 | requestUri?: { 234 | enabled?: boolean 235 | requireUriRegistration?: boolean 236 | } 237 | resourceIndicator?: FeatureConfiguration 238 | revocation?: FeatureConfiguration 239 | sessionManagement?: { 240 | enabled?: boolean 241 | keepHeaders?: boolean 242 | } 243 | userInfo?: FeatureConfiguration 244 | webMessageResponseMode?: FeatureConfiguration 245 | } 246 | acrValues?: string[] 247 | audiences?: (ctx: Context, sub: string, token: string, use: 'access_token' | 'client_credentials') => boolean | undefined | string[] 248 | claims?: { [claim: string]: string[] } | string[] 249 | clientBasedCORS?: (ctx: Context, origin: string, client: Client) => Promise 250 | clientDefaults?: Client 251 | clockTolerance?: number 252 | conformIdTokenClaims?: boolean 253 | cookies?: { 254 | keys: Keygrip[] 255 | long: { 256 | httpOnly?: boolean 257 | maxAge?: number 258 | overwrite?: boolean 259 | // TODO: find out the valid strings for same site 260 | sameSite?: 'none' | 'lax' | 'strict' 261 | } 262 | names?: { 263 | interaction?: string 264 | resume?: string 265 | session?: string 266 | state?: string 267 | } 268 | short?: { 269 | httpsOnly?: boolean 270 | maxAge?: number 271 | overwrite?: boolean 272 | sameSite?: 'none' | 'lax' | 'strict' 273 | } 274 | } 275 | discovery?: { 276 | [property: string]: any 277 | } 278 | dynamicScopes?: RegExp[] 279 | expiresWithSession?: (ctx: Context, token: string) => Promise 280 | extraAccessTokenClaims?: (ctx: Context, token: string) => Promise<{ [key: string]: string }> 281 | extraClientMetadata?: { 282 | properties?: string[] 283 | validator?: (key: string, value: any, metadata: { [key: string]: any }) => void 284 | } 285 | extraParams?: string[] 286 | formats?: { 287 | // TODO What does the client_credientials configuration look like (not listed here yet but referenced in documentation) 288 | jwtAccessTokenSigningAlg?: (ctx: Context, token: string, client: Client) => SigningAlgValue 289 | AccessToken?: 'jwt' | 'opaque' | 'paseto' | ((ctx: Context, token: string) => 'jwt' | 'opaque') 290 | default?: 'jwt' | 'opaque' | 'paseto' | ((ctx: Context, token: string) => 'jwt' | 'opaque') 291 | } 292 | httpsOptions?: (options: GotOptions) => GotOptions 293 | interactions?: { 294 | policy?: interactionPolicy.Prompt[] 295 | url?: (ctx: Context, interaction?: string) => Promise 296 | } 297 | introspectionEndpointAuthMethods?: AuthMethod[] 298 | issueRefreshToken?: (ctx: Context, client: Client, code: string) => Promise 299 | logoutSource?: (ctx: Context, form: string) => Promise 300 | pairwiseIdentifier?: (ctx: Context, accountId: string, client: any) => Promise 301 | pkceMethods?: ('S256' | 'plain')[] 302 | postLogoutSuccessSource?: (ctx: Context) => string 303 | renderError?: (ctx: Context, out: { [errorName: string]: string }, error: Error) => string 304 | responseTypes?: string[] 305 | revocationEndpointAuthMethods?: AuthMethod[] 306 | rotateRefreshToken?: boolean | ((ctx: Context) => boolean) 307 | routes?: { 308 | authorization?: string 309 | check_session?: string 310 | code_verification?: string 311 | device_authorization?: string 312 | end_session?: string 313 | introspection?: string 314 | jwks?: string 315 | registration?: string 316 | revocation?: string 317 | token?: string 318 | userinfo?: string 319 | } 320 | scopes?: string[] 321 | subjectTypes?: SubjectType 322 | tokenEndpointAuthMethods?: AuthMethod[] 323 | ttl?: { 324 | AccessToken?: number | ((ctx: Context, token: string, client: Client) => number) 325 | AuthorizationCode?: number | ((ctx: Context, token: string, client: Client) => number) 326 | ClientCredentials?: number | ((ctx: Context, token: string, client: Client) => number) 327 | DeviceCode?: number | ((ctx: Context, token: string, client: Client) => number) 328 | IdToken?: number | ((ctx: Context, token: string, client: Client) => number) 329 | RefreshToken?: number | ((ctx: Context, token: string, client: Client) => number) 330 | } 331 | whitelistedJWA?: { 332 | authorizationEncryptionAlgValues?: EncryptionAlgValue[] 333 | authorizationEncryptionEncValues?: EncryptionEncValue[] 334 | authorizationSigningAlgValues?: SigningAlgValue[] 335 | dPoPSigningAlgValues?: SigningAlgValue[] 336 | idTokenEncryptionAlgValues?: EncryptionAlgValue[] 337 | idTokenEncryptionEncValues?: EncryptionEncValue[] 338 | idTokenSigningAlgValues?: ('none' | SigningAlgValue)[] 339 | introspectionEncryptionAlgValues?: EncryptionAlgValue[] 340 | introspectionEncryptionEncValues?: EncryptionEncValue[] 341 | introspectionEndpointAuthSigningAlgValues?: SigningAlgValue[] 342 | introspectionSigningAlgValues?: ('none' | SigningAlgValue)[] 343 | requestObjectEncryptionAlgValues?: EncryptionAlgValue[] 344 | requestObjectEncryptionEncValues?: EncryptionEncValue[] 345 | requestObjectSigningAlgValues?: ('none' | SigningAlgValue)[] 346 | revocationEndpointAuthSigningAlgValues?: SigningAlgValue[] 347 | tokenEndpointAuthSigningAlgValues?: SigningAlgValue[] 348 | userinfoEncryptionAlgValues?: EncryptionAlgValue[] 349 | userinfoEncryptionEncValues?: EncryptionEncValue[] 350 | userinfoSigningAlgValues?: ('none' | SigningAlgValue)[] 351 | } 352 | } 353 | 354 | // TODO: guarntee this is what a session looks like 355 | interface Session { 356 | _id: string; 357 | accountId: string | null; 358 | expiresAt: Date; 359 | save(time: number): Promise; 360 | sidFor(client_id: string): boolean; 361 | login: {}; 362 | interaction: { 363 | error?: "login_required"; 364 | error_description: string; 365 | reason: "no_session" | "consent_prompt" | "client_not_authorized"; 366 | reason_description: string; 367 | }; 368 | params: { 369 | client_id: string; 370 | redirect_uri: string; 371 | response_mode: "query"; 372 | response_type: "code"; 373 | login_hint?: string; 374 | scope: "openid"; 375 | state: string; 376 | }; 377 | returnTo: string; 378 | signed: null; 379 | uuid: string; 380 | id: string; 381 | } 382 | 383 | interface InteractionResult { 384 | login?: { 385 | account: string, 386 | acr?: string, 387 | remember?: boolean, 388 | ts?: number 389 | } 390 | consent?: { 391 | rejectedScopes?: string[] 392 | rejectedClaims?: string[] 393 | } 394 | meta?: {} 395 | error?: string 396 | error_description?: string 397 | } 398 | 399 | class OIDCProviderError extends Error { 400 | name: string; 401 | message: string; 402 | error: string; 403 | status: number; 404 | statusCode: number; 405 | expose: boolean 406 | constructor(status: number, message: string) 407 | } 408 | 409 | // TODO confirm that the error namespace matches up properly 410 | namespace errors { 411 | 412 | class AccessDenied extends OIDCProviderError { 413 | constructor(description: string, detail: any) 414 | } 415 | 416 | class AuthorizationPending extends OIDCProviderError { 417 | constructor(description: string, detail: any) 418 | } 419 | 420 | class ConsentRequired extends OIDCProviderError { 421 | constructor(description: string, detail: any) 422 | } 423 | 424 | class ExpiredToken extends OIDCProviderError { 425 | constructor(description: string, detail: any) 426 | } 427 | 428 | class InteractionRequired extends OIDCProviderError { 429 | constructor(description: string, detail: any) 430 | } 431 | 432 | class InvalidClient extends OIDCProviderError { 433 | constructor(detail: any) 434 | } 435 | 436 | class InvalidClientAuth extends OIDCProviderError { 437 | constructor(detail: any) 438 | } 439 | 440 | class InvalidClientMetadata extends OIDCProviderError { 441 | constructor(description: string) 442 | } 443 | 444 | class InvalidGrant extends OIDCProviderError { 445 | constructor(detail: any) 446 | } 447 | 448 | class InvalidRequest extends OIDCProviderError { 449 | constructor(description: string, code: number) 450 | } 451 | 452 | class InvalidRequestObject extends OIDCProviderError { 453 | constructor(description: string, detail: any) 454 | } 455 | 456 | class InvalidRequestUri extends OIDCProviderError { 457 | constructor(description: string, detail: any) 458 | } 459 | 460 | class InvalidScope extends OIDCProviderError { 461 | constructor(description: string, scope: string) 462 | } 463 | 464 | class InvalidSoftwareStatement extends OIDCProviderError { 465 | constructor(description: string, detail: any) 466 | } 467 | 468 | class InvalidTarget extends OIDCProviderError { 469 | constructor(description: string, detail: any) 470 | } 471 | 472 | class InvalidToken extends OIDCProviderError { 473 | constructor(detail: any) 474 | } 475 | 476 | class LoginRequired extends OIDCProviderError { 477 | constructor(description: string, detail: any) 478 | } 479 | 480 | class RedirectUriMismatch extends OIDCProviderError { 481 | constructor(description: string, detail: any) 482 | } 483 | 484 | class RegistrationNotSupported extends OIDCProviderError { 485 | constructor(description: string, detail: any) 486 | } 487 | 488 | class RequestNotSupported extends OIDCProviderError { 489 | constructor(description: string, detail: any) 490 | } 491 | 492 | class RequestUriNotSupported extends OIDCProviderError { 493 | constructor(description: string, detail: any) 494 | } 495 | 496 | class SessionNotFound extends InvalidRequest { } 497 | 498 | class SlowDown extends OIDCProviderError { 499 | constructor(description: string, detail: any) 500 | } 501 | 502 | class TemporarilyUnavailable extends OIDCProviderError { 503 | constructor(description: string, detail: any) 504 | } 505 | 506 | class UnapprovedSoftwareStatement extends OIDCProviderError { 507 | constructor(description: string, detail: any) 508 | } 509 | 510 | class UnauthorizedClient extends OIDCProviderError { 511 | constructor(description: string, detail: any) 512 | } 513 | 514 | class UnsupportedGrantType extends OIDCProviderError { 515 | constructor(description: string, detail: any) 516 | } 517 | 518 | class UnsupportedResponseMode extends OIDCProviderError { 519 | constructor(description: string, detail: any) 520 | } 521 | 522 | class UnsupportedResponseType extends OIDCProviderError { 523 | constructor(description: string, detail: any) 524 | } 525 | 526 | class WebMessageUriMismatch extends OIDCProviderError { 527 | constructor(description: string, detail: any) 528 | } 529 | 530 | } 531 | 532 | namespace interactionPolicy { 533 | class Check { 534 | reason: string 535 | description: string 536 | error: Error 537 | check: (ctx: Context) => boolean 538 | details: any 539 | constructor( 540 | reason: string, 541 | description: string, 542 | error: Error, 543 | check: (ctx: Context) => boolean, 544 | details: any 545 | ) 546 | } 547 | 548 | class Prompt { 549 | name: string 550 | rquestable: boolean 551 | checks: Check[] 552 | constructor(options: { name?: string, requestable?: boolean }, details: any, ...checks: Check[]) 553 | // TODO is it possible to get a more specific return type? 554 | details: (ctx: Context) => { [key: string]: any } 555 | } 556 | 557 | // TODO: fill out what base returns 558 | function base(): any; 559 | 560 | } 561 | } 562 | } 563 | --------------------------------------------------------------------------------