├── .adonisrc.json ├── .dockerignore ├── .editorconfig ├── .env.example ├── .env.test ├── .gitignore ├── .prettierignore ├── Dockerfile ├── LICENSE ├── README.md ├── ace ├── ace-manifest.json ├── app ├── Exceptions │ └── Handler.ts └── Services │ ├── NostrPool.ts │ ├── NostrTools.ts │ ├── Simplepool.ts │ ├── WebSocketHandler.ts │ └── WebSocketInstance.ts ├── commands └── index.ts ├── config ├── app.ts ├── bodyparser.ts ├── cors.ts ├── drive.ts ├── hash.ts ├── session.ts ├── shield.ts └── static.ts ├── contracts ├── drive.ts ├── env.ts ├── events.ts ├── hash.ts └── tests.ts ├── docker-compose.dev.yml ├── env.ts ├── package.json ├── pnpm-lock.yaml ├── providers └── AppProvider.ts ├── public ├── assets │ ├── entrypoints.json │ └── manifest.json └── favicon.ico ├── resources ├── css │ └── app.css ├── js │ └── app.js └── views │ ├── errors │ ├── not-found.edge │ ├── server-error.edge │ └── unauthorized.edge │ └── welcome.edge ├── server.ts ├── start ├── cron.ts ├── kernel.ts ├── routes.ts └── socket.ts ├── stress-test.js ├── test.ts ├── tests ├── bootstrap.ts └── functional │ └── hello_world.spec.ts ├── tsconfig.json └── webpack.config.js /.adonisrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": true, 3 | "commands": [ 4 | "./commands", 5 | "@adonisjs/core/build/commands/index.js", 6 | "@adonisjs/repl/build/commands" 7 | ], 8 | "exceptionHandlerNamespace": "App/Exceptions/Handler", 9 | "aliases": { 10 | "App": "app", 11 | "Config": "config", 12 | "Database": "database", 13 | "Contracts": "contracts" 14 | }, 15 | "preloads": [ 16 | "./start/routes", 17 | "./start/kernel" 18 | ], 19 | "providers": [ 20 | "./providers/AppProvider", 21 | "@adonisjs/core", 22 | "@adonisjs/session", 23 | "@adonisjs/view", 24 | "@adonisjs/shield" 25 | ], 26 | "metaFiles": [ 27 | { 28 | "pattern": "public/**", 29 | "reloadServer": false 30 | }, 31 | { 32 | "pattern": "resources/views/**/*.edge", 33 | "reloadServer": false 34 | } 35 | ], 36 | "aceProviders": [ 37 | "@adonisjs/repl" 38 | ], 39 | "tests": { 40 | "suites": [ 41 | { 42 | "name": "functional", 43 | "files": [ 44 | "tests/functional/**/*.spec(.ts|.js)" 45 | ], 46 | "timeout": 60000 47 | } 48 | ] 49 | }, 50 | "testProviders": [ 51 | "@japa/preset-adonis/TestsProvider" 52 | ] 53 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Adonis default .gitignore ignores 2 | node_modules 3 | build 4 | coverage 5 | .vscode 6 | .DS_STORE 7 | .env 8 | tmp 9 | 10 | # Additional .gitignore ignores (any custom file you wish) 11 | .idea 12 | 13 | # Additional good to have ignores for dockerignore 14 | Dockerfile* 15 | docker-compose* 16 | .dockerignore 17 | *.md 18 | .git 19 | .gitignore -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = false 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3333 2 | HOST=0.0.0.0 3 | NODE_ENV=development 4 | APP_KEY=nP8HxRzja-sysBG6rn155do38mWGa9ZQ 5 | DRIVE_DISK=local 6 | SESSION_DRIVER=cookie 7 | CACHE_VIEWS=false 8 | PROXY_URL=wss://nproxy.zerologin.co 9 | RELAYS=wss://nostr.v0l.io,wss://relay.nostr.bg,wss://relay.damus.io,wss://nostr-pub.wellorder.net,wss://nostr.zebedee.cloud,wss://nostr.openchain.fr,wss://nostr.bitcoiner.social,wss://nostr-pub.semisol.dev/,wss://nos.lol -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | ASSETS_DRIVER=fake 3 | SESSION_DRIVER=memory 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | .vscode 5 | .DS_STORE 6 | .env 7 | tmp 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_IMAGE=node:16.13.1-alpine 2 | 3 | FROM $NODE_IMAGE AS base 4 | RUN apk --no-cache add dumb-init curl python3 make gcc g++ 5 | RUN mkdir -p /home/node/app && chown node:node /home/node/app 6 | WORKDIR /home/node/app 7 | RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm 8 | USER node 9 | RUN mkdir tmp 10 | 11 | FROM base AS dependencies 12 | COPY --chown=node:node ./package.json ./ 13 | COPY --chown=node:node ./pnpm-lock.yaml ./ 14 | RUN pnpm install 15 | COPY --chown=node:node . . 16 | 17 | FROM dependencies AS build 18 | RUN node ace build --production --ignore-ts-errors 19 | 20 | FROM base AS production 21 | ENV NODE_ENV=production 22 | ENV PORT=$PORT 23 | ENV HOST=0.0.0.0 24 | COPY --chown=node:node ./package*.json ./ 25 | RUN pnpm install --prod 26 | COPY --chown=node:node --from=build /home/node/app/build . 27 | EXPOSE $PORT 28 | CMD [ "dumb-init", "node", "server.js" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dolu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nostr Proxy 2 | Push and get events to your Proxy, get results from multiple Nostr relays 3 | 4 | ## Installation 5 | 6 | In the project directory, run: 7 | 8 | ```sh 9 | pnpm install 10 | ``` 11 | 12 | Edit your env variables. You can use a `.env` file by copying the file `.env.example` in the root directory, or you can set your variables into your hosting provider UI.\ 13 | For `APP_KEY`, you can use the following command to generate one `node ace generate:key` 14 | 15 | ``` 16 | PORT=3333 17 | HOST=0.0.0.0 18 | NODE_ENV=development 19 | APP_KEY=unique-key 20 | DRIVE_DISK=local 21 | SESSION_DRIVER=cookie 22 | CACHE_VIEWS=false 23 | PROXY_URL=wss://your-proxy.com 24 | RELAYS=wss://relay1.com,wss://relay2.com,wss://relay.com 25 | ``` 26 | 27 | ## Launch 28 | 29 | ### Development 30 | 31 | `npm run dev` to start the app in dev mode.\ 32 | Open [http://localhost:3333](http://localhost:3333) to view it in the browser.\ 33 | Use `ws://localhost:3333` into your Nostr client. 34 | 35 | ### Production 36 | 37 | ``` 38 | npm run build 39 | cd build 40 | pnpm install --prod 41 | node server.js 42 | ``` 43 | 44 | ### Running tests 45 | 46 | TODO 47 | 48 | ## Known issues 49 | - None? 50 | 51 | ## Learn More 52 | 53 | - [Nostr](https://github.com/nostr-protocol/nostr) 54 | - [Awesome Nostr](https://github.com/aljazceru/awesome-nostr) 55 | 56 | ## License 57 | 58 | This project is MIT licensed. 59 | -------------------------------------------------------------------------------- /ace: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Ace Commands 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is the entry point for running ace commands. 7 | | 8 | */ 9 | 10 | require('reflect-metadata') 11 | require('source-map-support').install({ handleUncaughtExceptions: false }) 12 | 13 | const { Ignitor } = require('@adonisjs/core/build/standalone') 14 | new Ignitor(__dirname) 15 | .ace() 16 | .handle(process.argv.slice(2)) 17 | -------------------------------------------------------------------------------- /ace-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | "dump:rcfile": { 4 | "settings": {}, 5 | "commandPath": "@adonisjs/core/build/commands/DumpRc", 6 | "commandName": "dump:rcfile", 7 | "description": "Dump contents of .adonisrc.json file along with defaults", 8 | "args": [], 9 | "aliases": [], 10 | "flags": [] 11 | }, 12 | "list:routes": { 13 | "settings": { 14 | "loadApp": true, 15 | "stayAlive": true 16 | }, 17 | "commandPath": "@adonisjs/core/build/commands/ListRoutes/index", 18 | "commandName": "list:routes", 19 | "description": "List application routes", 20 | "args": [], 21 | "aliases": [], 22 | "flags": [ 23 | { 24 | "name": "verbose", 25 | "propertyName": "verbose", 26 | "type": "boolean", 27 | "description": "Display more information" 28 | }, 29 | { 30 | "name": "reverse", 31 | "propertyName": "reverse", 32 | "type": "boolean", 33 | "alias": "r", 34 | "description": "Reverse routes display" 35 | }, 36 | { 37 | "name": "methods", 38 | "propertyName": "methodsFilter", 39 | "type": "array", 40 | "alias": "m", 41 | "description": "Filter routes by method" 42 | }, 43 | { 44 | "name": "patterns", 45 | "propertyName": "patternsFilter", 46 | "type": "array", 47 | "alias": "p", 48 | "description": "Filter routes by the route pattern" 49 | }, 50 | { 51 | "name": "names", 52 | "propertyName": "namesFilter", 53 | "type": "array", 54 | "alias": "n", 55 | "description": "Filter routes by route name" 56 | }, 57 | { 58 | "name": "json", 59 | "propertyName": "json", 60 | "type": "boolean", 61 | "description": "Output as JSON" 62 | }, 63 | { 64 | "name": "table", 65 | "propertyName": "table", 66 | "type": "boolean", 67 | "description": "Output as Table" 68 | }, 69 | { 70 | "name": "max-width", 71 | "propertyName": "maxWidth", 72 | "type": "number", 73 | "description": "Specify maximum rendering width. Ignored for JSON Output" 74 | } 75 | ] 76 | }, 77 | "generate:key": { 78 | "settings": {}, 79 | "commandPath": "@adonisjs/core/build/commands/GenerateKey", 80 | "commandName": "generate:key", 81 | "description": "Generate a new APP_KEY secret", 82 | "args": [], 83 | "aliases": [], 84 | "flags": [] 85 | }, 86 | "repl": { 87 | "settings": { 88 | "loadApp": true, 89 | "environment": "repl", 90 | "stayAlive": true 91 | }, 92 | "commandPath": "@adonisjs/repl/build/commands/AdonisRepl", 93 | "commandName": "repl", 94 | "description": "Start a new REPL session", 95 | "args": [], 96 | "aliases": [], 97 | "flags": [] 98 | } 99 | }, 100 | "aliases": {} 101 | } 102 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Http Exception Handler 4 | |-------------------------------------------------------------------------- 5 | | 6 | | AdonisJs will forward all exceptions occurred during an HTTP request to 7 | | the following class. You can learn more about exception handling by 8 | | reading docs. 9 | | 10 | | The exception handler extends a base `HttpExceptionHandler` which is not 11 | | mandatory, however it can do lot of heavy lifting to handle the errors 12 | | properly. 13 | | 14 | */ 15 | 16 | import Logger from '@ioc:Adonis/Core/Logger' 17 | import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler' 18 | 19 | export default class ExceptionHandler extends HttpExceptionHandler { 20 | protected statusPages = { 21 | '403': 'errors/unauthorized', 22 | '404': 'errors/not-found', 23 | '500..599': 'errors/server-error', 24 | } 25 | 26 | constructor() { 27 | super(Logger) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Services/NostrPool.ts: -------------------------------------------------------------------------------- 1 | // Fail attempt to create a pool of relays to avoid memory leak. 2 | 3 | // import { Filter, Relay, Sub, Event, relayInit } from "@dolu/nostr-tools" 4 | // import EventEmitter from "node:events" 5 | // import Env from "@ioc:Adonis/Core/Env" 6 | // import { normalizeURL } from "./NostrTools" 7 | 8 | // class NostrPool { 9 | // public isInitialized: boolean 10 | // private _relaysUrls: string[] 11 | // private _connections: { [url: string]: Relay } 12 | 13 | // constructor(relayUrls: string[]) { 14 | // this.isInitialized = false 15 | // this._relaysUrls = relayUrls 16 | // this._connections = {} 17 | // } 18 | 19 | // private _verifyInitializedOrDie() { 20 | // if (!this.isInitialized) throw new Error('NostrPool not initialized. Call init() first.') 21 | // } 22 | 23 | // public async init() { 24 | // for (const relayUrl of this._relaysUrls) { 25 | // await this._openRelayConnection(relayUrl, true) 26 | // } 27 | // this.isInitialized = true 28 | // } 29 | 30 | // private async _ensureRelayConnections(relays: string[]) { 31 | // for (const relayUrl of relays) { 32 | // await this._openRelayConnection(relayUrl) 33 | // } 34 | // } 35 | 36 | // private async _openRelayConnection(relayUrl: string, fromInit: boolean = false) { 37 | // const normalizedURL = normalizeURL(relayUrl) 38 | 39 | // if (this._connections[normalizedURL]?.status === 1) return 40 | 41 | // try { 42 | // this._connections[normalizedURL] = relayInit(normalizedURL) 43 | // await this._connections[normalizedURL].connect() 44 | 45 | // this._connections[normalizedURL].on('connect', () => { 46 | // console.log(`Connected to ${this._connections[normalizedURL].url}`) 47 | // }) 48 | // this._connections[normalizedURL].on('error', () => { 49 | // throw new Error('Failed to connect to relay') 50 | // }) 51 | // } catch (_) { 52 | // if (fromInit) console.error(`Error while initializing relay ${normalizedURL}.`) 53 | // } 54 | // } 55 | 56 | // public async sub(relays: string[], filters: Filter[], subscriptionId: string): Promise { 57 | // this._verifyInitializedOrDie() 58 | // // await this._ensureRelayConnections(relays) 59 | 60 | // const normalizedURLs = relays.map(normalizeURL) 61 | // const _subs = new Set() 62 | // let eoseCount = 0 63 | // let eoseTimeout = false 64 | 65 | // const emitter = new EventEmitter() 66 | 67 | // emitter.on('unsubscribe', async () => { 68 | // emitter.removeAllListeners() 69 | // _subs.forEach(sub => { 70 | // sub.unsub() 71 | // }) 72 | // for (const relayUrl of normalizedURLs) { 73 | // await Redis.del(`seen-on:${relayUrl}:${subscriptionId}`) 74 | // } 75 | // _subs.clear() 76 | // }) 77 | 78 | // for (const normalizedURL of normalizedURLs) { 79 | // let conn = this._connections[normalizedURL] 80 | // if (conn?.status !== 1) continue 81 | 82 | // const sub = conn.sub(filters, { 83 | // id: subscriptionId, 84 | // alreadyHaveEvent: async (id: string, relayUrl: string) => { 85 | // return await Redis.sismember(`seen-on:${relayUrl}:${subscriptionId}`, id) === 1 86 | // }, 87 | // skipVerification: true 88 | // }) 89 | 90 | // sub.on('event', async (event: Event) => { 91 | // if (await Redis.sismember(`seen-on:${normalizedURL}:${subscriptionId}`, event.id as string) === 1) return 92 | 93 | // await Redis.sadd(`seen-on:${normalizedURL}:${subscriptionId}`, event.id as string) 94 | // emitter.emit('event', event) 95 | // }) 96 | 97 | // sub.on('eose', () => { 98 | // eoseCount++ 99 | // if (eoseCount === 1) { 100 | // const timer = setTimeout(() => { 101 | // emitter.emit('eose') 102 | // eoseTimeout = true 103 | // clearTimeout(timer) 104 | // }, 2500) 105 | // } 106 | // if (eoseCount === this._countConnectedRelays() && !eoseTimeout) { 107 | // emitter.emit('eose') 108 | // } 109 | // }) 110 | // _subs.add(sub) 111 | // } 112 | 113 | // return emitter 114 | // } 115 | 116 | // // There is no solution currently to unsubscribe from a publish. It creates a memory leak. 117 | // // TODO : Find a solution to unsubscribe from a publish in nostr-tools 118 | // public async publish(relays: string[], event: Event): Promise { 119 | // this._verifyInitializedOrDie() 120 | // // await this._ensureRelayConnections(relays) 121 | 122 | // const emitter = new EventEmitter() 123 | 124 | // emitter.on('unsubscribe', () => { 125 | // emitter.removeAllListeners() 126 | // }) 127 | 128 | // let seenOn = 0 129 | // let seenOnTimeout = false 130 | 131 | // for (const relay of relays) { 132 | // let conn = this._connections[normalizeURL(relay)] 133 | // if (conn?.status !== 1) continue 134 | 135 | // let pub = conn.publish(event) 136 | // pub.on('ok', () => { 137 | // seenOn++ 138 | // if (seenOn === 1) { 139 | // const timer = setTimeout(() => { 140 | // seenOnTimeout = true 141 | // emitter?.emit('ok') 142 | // emitter?.emit('unsubscribe') 143 | // clearTimeout(timer) 144 | // }, 2500) 145 | // } 146 | // if (seenOn === this._countConnectedRelays() && !seenOnTimeout) { 147 | // emitter?.emit('ok') 148 | // emitter?.emit('unsubscribe') 149 | // } 150 | // }) 151 | // pub.on('failed', (reason: string) => { 152 | // seenOn++ 153 | // emitter?.emit('failed', reason) 154 | // const timer = setTimeout(() => { 155 | // emitter?.emit('unsubscribe') 156 | // clearTimeout(timer) 157 | // }, 2500) 158 | // }) 159 | // } 160 | 161 | // return emitter 162 | // } 163 | 164 | // private _countConnectedRelays(): number { 165 | // this._verifyInitializedOrDie() 166 | // return Object.values(this._connections).filter(conn => conn.status === 1).length 167 | // } 168 | 169 | // public getRelays(): string[] { 170 | // this._verifyInitializedOrDie() 171 | // return Object.keys(this._connections) 172 | // } 173 | 174 | // public getRelaysStatus(): { url: string, connected: boolean }[] { 175 | // this._verifyInitializedOrDie() 176 | // return Object.entries(this._connections).map(([url, conn]) => { 177 | // return { 178 | // url, 179 | // connected: conn.status === 1 180 | // } 181 | // }) 182 | // } 183 | // } 184 | 185 | // export default new NostrPool([...Env.get('RELAYS').split(',')]) -------------------------------------------------------------------------------- /app/Services/NostrTools.ts: -------------------------------------------------------------------------------- 1 | export const utf8Decoder = new TextDecoder('utf-8') 2 | export const utf8Encoder = new TextEncoder() 3 | 4 | export function normalizeURL(url: string): string { 5 | let p = new URL(url) 6 | p.pathname = p.pathname.replace(/\/+/g, '/') 7 | if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1) 8 | if ( 9 | (p.port === '80' && p.protocol === 'ws:') || 10 | (p.port === '443' && p.protocol === 'wss:') 11 | ) 12 | p.port = '' 13 | p.searchParams.sort() 14 | p.hash = '' 15 | return p.toString() 16 | } 17 | 18 | export function getHex64(json: string, field: string): string { 19 | let len = field.length + 3 20 | let idx = json.indexOf(`"${field}":`) + len 21 | let s = json.slice(idx).indexOf(`"`) + idx + 1 22 | return json.slice(s, s + 64) 23 | } 24 | 25 | export function getInt(json: string, field: string): number { 26 | let len = field.length 27 | let idx = json.indexOf(`"${field}":`) + len + 3 28 | let sliced = json.slice(idx) 29 | let end = Math.min(sliced.indexOf(','), sliced.indexOf('}')) 30 | return parseInt(sliced.slice(0, end), 10) 31 | } 32 | 33 | export function getSubscriptionId(json: string): string | null { 34 | let idx = json.slice(0, 22).indexOf(`"EVENT"`) 35 | if (idx === -1) return null 36 | 37 | let pstart = json.slice(idx + 7 + 1).indexOf(`"`) 38 | if (pstart === -1) return null 39 | let start = idx + 7 + 1 + pstart 40 | 41 | let pend = json.slice(start + 1, 80).indexOf(`"`) 42 | if (pend === -1) return null 43 | let end = start + 1 + pend 44 | 45 | return json.slice(start + 1, end) 46 | } 47 | 48 | export function matchEventId(json: string, id: string): boolean { 49 | return id === getHex64(json, 'id') 50 | } 51 | 52 | export function matchEventPubkey(json: string, pubkey: string): boolean { 53 | return pubkey === getHex64(json, 'pubkey') 54 | } 55 | 56 | export function matchEventKind(json: string, kind: number): boolean { 57 | return kind === getInt(json, 'kind') 58 | } 59 | -------------------------------------------------------------------------------- /app/Services/Simplepool.ts: -------------------------------------------------------------------------------- 1 | // import { Pub, Relay, relayInit, SubscriptionOptions, Filter, Event, Sub } from "nostr-tools" 2 | import { Filter, Relay, relayInit, Sub, SubscriptionOptions, Event, Pub } from "@dolu/nostr-tools" 3 | // import { Pub, Relay, relayInit, Sub, SubscriptionOptions } from "../Models/Relay" 4 | // import { Event } from "../Models/Event" 5 | import { normalizeURL } from "./NostrTools" 6 | 7 | export class SimplePool { 8 | private _conn: { [url: string]: Relay } 9 | 10 | constructor() { 11 | this._conn = {} 12 | } 13 | 14 | async close(relays: string[]): Promise { 15 | await Promise.all( 16 | relays.map(async url => { 17 | let relay = this._conn[normalizeURL(url)] 18 | if (relay) await relay.close() 19 | }) 20 | ) 21 | } 22 | 23 | async ensureRelay(url: string): Promise { 24 | const nm = normalizeURL(url) 25 | const existing = this._conn[nm] 26 | if (existing) return existing 27 | 28 | const relay = relayInit(nm) 29 | this._conn[nm] = relay 30 | 31 | await relay.connect() 32 | 33 | return relay 34 | } 35 | 36 | private _alreadyHaveEvent(id: string, _: string, knownIds: Set): boolean { 37 | return knownIds.has(id) 38 | } 39 | 40 | sub(relays: string[], filters: Filter[], opts?: SubscriptionOptions): Sub { 41 | let knownIds = new Set() 42 | 43 | let modifiedOpts = opts || {} 44 | modifiedOpts.alreadyHaveEvent = async (id, _) => this._alreadyHaveEvent(id, _, knownIds) 45 | modifiedOpts.skipVerification = true 46 | 47 | let subs: Sub[] = [] 48 | let eventListeners: Set<(event: Event) => void> = new Set() 49 | let eoseListeners: Set<() => void> = new Set() 50 | let eosesMissing = relays.length 51 | 52 | let eoseSent = false 53 | let eoseTimeout = setTimeout(() => { 54 | eoseSent = true 55 | for (let cb of eoseListeners.values()) cb() 56 | }, 2400) 57 | 58 | relays.forEach(async relay => { 59 | let r = await this.ensureRelay(relay) 60 | if (!r) return 61 | let s = r.sub(filters, modifiedOpts) 62 | s.on('event', async (event: Event) => { 63 | if (!knownIds.has(event.id as string)) { 64 | knownIds.add(event.id as string) 65 | for (let cb of eventListeners.values()) cb(event) 66 | } 67 | }) 68 | s.on('eose', () => { 69 | knownIds.clear() 70 | if (eoseSent) return 71 | 72 | eosesMissing-- 73 | if (eosesMissing === 0) { 74 | clearTimeout(eoseTimeout) 75 | for (let cb of eoseListeners.values()) cb() 76 | } 77 | }) 78 | subs.push(s) 79 | }) 80 | 81 | let greaterSub: Sub = { 82 | sub(filters, opts) { 83 | subs.forEach(sub => sub.sub(filters, opts)) 84 | return greaterSub 85 | }, 86 | async unsub() { 87 | subs.forEach(sub => sub.unsub()) 88 | eventListeners.clear() 89 | eoseListeners.clear() 90 | knownIds.clear() 91 | }, 92 | on(type, cb) { 93 | switch (type) { 94 | case 'event': 95 | eventListeners.add(cb) 96 | break 97 | case 'eose': 98 | eoseListeners.add(cb) 99 | break 100 | } 101 | }, 102 | off(type, cb) { 103 | if (type === 'event') { 104 | eventListeners.delete(cb) 105 | } else if (type === 'eose') eoseListeners.delete(cb) 106 | } 107 | } 108 | 109 | return greaterSub 110 | } 111 | 112 | get( 113 | relays: string[], 114 | filter: Filter, 115 | opts?: SubscriptionOptions 116 | ): Promise { 117 | return new Promise(resolve => { 118 | let sub = this.sub(relays, [filter], opts) 119 | let timeout = setTimeout(() => { 120 | sub.unsub() 121 | resolve(null) 122 | }, 1500) 123 | sub.on('event', (event: Event) => { 124 | resolve(event) 125 | clearTimeout(timeout) 126 | sub.unsub() 127 | }) 128 | }) 129 | } 130 | 131 | list( 132 | relays: string[], 133 | filters: Filter[], 134 | opts?: SubscriptionOptions 135 | ): Promise { 136 | return new Promise(resolve => { 137 | let events: Event[] = [] 138 | let sub = this.sub(relays, filters, opts) 139 | 140 | sub.on('event', (event: Event) => { 141 | events.push(event) 142 | }) 143 | 144 | // we can rely on an eose being emitted here because pool.sub() will fake one 145 | sub.on('eose', () => { 146 | sub.unsub() 147 | resolve(events) 148 | }) 149 | }) 150 | } 151 | 152 | publish(relays: string[], event: Event): Pub[] { 153 | return relays.map(relay => { 154 | let r = this._conn[normalizeURL(relay)] 155 | if (!r) return badPub(relay) 156 | let s = r.publish(event) 157 | return s 158 | }) 159 | } 160 | 161 | } 162 | 163 | function badPub(relay: string): Pub { 164 | return { 165 | on(typ, cb) { 166 | if (typ === 'failed') cb(`relay ${relay} not connected`) 167 | }, 168 | off() { }, 169 | unpub() { }, 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/Services/WebSocketHandler.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | import Env from "@ioc:Adonis/Core/Env" 3 | import { WebSocket } from "ws"; 4 | import WebSocketInstance from "./WebSocketInstance" 5 | import { RelayPool, Event } from "nostr-relaypool"; 6 | import { Filter } from "@dolu/nostr-tools"; 7 | // import { SimplePool } from "./Simplepool"; 8 | 9 | interface CustomWebsocket extends WebSocket { 10 | connectionId: string 11 | } 12 | 13 | class WebSocketServer { 14 | 15 | public booted: boolean 16 | 17 | private _relays: string[] 18 | private _pool: RelayPool 19 | private _cache: Map 20 | private _subs: Map void> 21 | 22 | constructor() { 23 | this.booted = false 24 | this._relays = [...Env.get('RELAYS').split(',')] 25 | this._pool = new RelayPool(this._relays, { keepSignature: true, dontLogSubscriptions: true, noCache: true, skipVerification: true }) 26 | 27 | this._cache = new Map() 28 | this._subs = new Map() 29 | } 30 | 31 | public async boot() { 32 | if (this.booted) return 33 | 34 | this._pool.onerror((err, relayUrl) => { 35 | console.log("RelayPool error", err, " from relay ", relayUrl); 36 | }); 37 | this._pool.onnotice((relayUrl, notice) => { 38 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 39 | }); 40 | await this.initWsHandler() 41 | this.booted = true 42 | } 43 | 44 | private async initWsHandler() { 45 | WebSocketInstance.ws.on('connection', (socket: CustomWebsocket) => { 46 | 47 | socket.connectionId = uuidv4() 48 | 49 | socket.on('message', async (data) => { 50 | try { 51 | const randomSubscriptionId = uuidv4() 52 | const parsed = JSON.parse(data.toString()) 53 | 54 | if (parsed[0] === 'REQ') { 55 | const filters = parsed[2] as unknown as Filter 56 | const subscriptionId = parsed[1] 57 | 58 | // Close old subscription if subscriptionId already exists 59 | const oldRandomSubscriptionId = this._cache.get(`${socket.connectionId}:${subscriptionId}`) 60 | if (oldRandomSubscriptionId) { 61 | const unsub = this._subs.get(oldRandomSubscriptionId) 62 | if (unsub) unsub() 63 | this._subs.delete(oldRandomSubscriptionId) 64 | } 65 | 66 | this._cache.set(`${socket.connectionId}:${subscriptionId}`, randomSubscriptionId) 67 | 68 | let eoseCount = 0 69 | let eoseSent = false 70 | const eoseTimer = setTimeout(() => { 71 | if (!eoseSent) socket.send(JSON.stringify(["EOSE", subscriptionId])) 72 | eoseSent = true 73 | eoseCount = this._relays.length 74 | clearTimeout(eoseTimer) 75 | }, 2500) 76 | const unsub = this._pool.subscribe( 77 | [filters], 78 | this._relays, 79 | (event, isAfterEose, relayURL) => { 80 | const { relayPool, relays, ...e } = event 81 | socket.send(JSON.stringify(["EVENT", subscriptionId, e])) 82 | }, 83 | undefined, 84 | (events, relayURL) => { 85 | eoseCount++ 86 | if (!eoseSent && eoseCount === this._relays.length) { 87 | eoseSent = true 88 | socket.send(JSON.stringify(["EOSE", subscriptionId])) 89 | } 90 | }, { logAllEvents: false } 91 | ) 92 | 93 | this._subs.set(randomSubscriptionId, unsub) 94 | } 95 | else if (parsed[0] === 'CLOSE') { 96 | const subscriptionId = parsed[1] 97 | 98 | const randomSubscriptionId = this._cache.get(`${socket.connectionId}:${subscriptionId}`) 99 | if (randomSubscriptionId) { 100 | const unsub = this._subs.get(randomSubscriptionId) 101 | if (unsub) unsub() 102 | this._subs.delete(randomSubscriptionId) 103 | 104 | this._cache.delete(`${socket.connectionId}:${subscriptionId}`) 105 | } 106 | } 107 | else if (parsed[0] === 'EVENT') { 108 | const event = parsed[1] as unknown as Event 109 | 110 | let publishConfrimed = false 111 | const unsubPublish = this._pool.subscribe( 112 | [{ ids: [event.id] }], 113 | this._relays, 114 | (event, isAfterEose, relayURL) => { 115 | if (!publishConfrimed) { 116 | socket.send(JSON.stringify(["OK", event.id, true, ""])) 117 | publishConfrimed = true 118 | unsubPublish() 119 | } 120 | }, 121 | undefined, 122 | undefined 123 | ) 124 | 125 | this._pool.publish(event, this._relays) 126 | 127 | const publishTimer = setTimeout(() => { 128 | if (!publishConfrimed) { 129 | socket.send(JSON.stringify(["NOTICE", "Event not published in time"])) 130 | unsubPublish() 131 | } 132 | clearTimeout(publishTimer) 133 | }, 2500) 134 | } 135 | else { 136 | throw new Error(`Invalid event ${data.toString()}`) 137 | } 138 | } catch (error) { 139 | console.error('Unexpected error in Socket message: ', data.toString(), error) 140 | for (const key of this._cache.keys()) { 141 | if (key.startsWith(socket.connectionId)) { 142 | const randomSubscriptionId = this._cache.get(key) 143 | if (randomSubscriptionId) { 144 | const unsub = this._subs.get(randomSubscriptionId) 145 | if (unsub) unsub() 146 | this._subs.delete(randomSubscriptionId) 147 | } 148 | this._cache.delete(key) 149 | } 150 | } 151 | socket.close(3000, error.message) 152 | } 153 | }) 154 | 155 | socket.on('close', async () => { 156 | for (const key of this._cache.keys()) { 157 | if (key.startsWith(socket.connectionId)) { 158 | const randomSubscriptionId = this._cache.get(key) 159 | if (randomSubscriptionId) { 160 | const unsub = this._subs.get(randomSubscriptionId) 161 | if (unsub) unsub() 162 | this._subs.delete(randomSubscriptionId) 163 | } 164 | this._cache.delete(key) 165 | } 166 | } 167 | }) 168 | }) 169 | } 170 | 171 | public async getRelays(): Promise<{ url: string, connected: boolean }[]> { 172 | let relays: { url: string, connected: boolean }[] = [] 173 | for (const [url, status] of this._pool.getRelayStatuses()) { 174 | relays = [...relays, { url: url, connected: status === 1 ? true : false }] 175 | } 176 | return relays 177 | } 178 | 179 | public async getStats() { 180 | const relays = await this.getRelays() 181 | return { 182 | connectedClients: WebSocketInstance.ws.clients.size, 183 | internalInfos: { 184 | subs: this._subs.size, 185 | cache: this._cache.size, 186 | }, 187 | relays: { 188 | connected: relays.filter(relay => relay.connected).length, 189 | total: relays.length 190 | } 191 | } 192 | } 193 | 194 | } 195 | 196 | export default new WebSocketServer() -------------------------------------------------------------------------------- /app/Services/WebSocketInstance.ts: -------------------------------------------------------------------------------- 1 | import ws from 'ws' 2 | import Server from '@ioc:Adonis/Core/Server' 3 | import WebSocketHandler from './WebSocketHandler' 4 | 5 | class Ws { 6 | public ws: ws.Server 7 | private booted = false 8 | public emit: Event 9 | 10 | public async boot() { 11 | /** 12 | * Ignore multiple calls to the boot method 13 | */ 14 | if (this.booted) { 15 | return 16 | } 17 | 18 | this.booted = true 19 | this.ws = new ws.Server({ server: Server.instance! }) 20 | await WebSocketHandler.boot() 21 | } 22 | } 23 | 24 | export default new Ws() -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | import { listDirectoryFiles } from '@adonisjs/core/build/standalone' 2 | import Application from '@ioc:Adonis/Core/Application' 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Exporting an array of commands 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Instead of manually exporting each file from this directory, we use the 10 | | helper `listDirectoryFiles` to recursively collect and export an array 11 | | of filenames. 12 | | 13 | | Couple of things to note: 14 | | 15 | | 1. The file path must be relative from the project root and not this directory. 16 | | 2. We must ignore this file to avoid getting into an infinite loop 17 | | 18 | */ 19 | export default listDirectoryFiles(__dirname, Application.appRoot, ['./commands/index']) 20 | -------------------------------------------------------------------------------- /config/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefZ 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import proxyAddr from 'proxy-addr' 9 | import Env from '@ioc:Adonis/Core/Env' 10 | import Application from '@ioc:Adonis/Core/Application' 11 | import type { ServerConfig } from '@ioc:Adonis/Core/Server' 12 | import type { LoggerConfig } from '@ioc:Adonis/Core/Logger' 13 | import type { ProfilerConfig } from '@ioc:Adonis/Core/Profiler' 14 | import type { ValidatorConfig } from '@ioc:Adonis/Core/Validator' 15 | import type { AssetsManagerConfig } from '@ioc:Adonis/Core/AssetsManager' 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Application secret key 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The secret to encrypt and sign different values in your application. 23 | | Make sure to keep the `APP_KEY` as an environment variable and secure. 24 | | 25 | | Note: Changing the application key for an existing app will make all 26 | | the cookies invalid and also the existing encrypted data will not 27 | | be decrypted. 28 | | 29 | */ 30 | export const appKey: string = Env.get('APP_KEY') 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Http server configuration 35 | |-------------------------------------------------------------------------- 36 | | 37 | | The configuration for the HTTP(s) server. Make sure to go through all 38 | | the config properties to make keep server secure. 39 | | 40 | */ 41 | export const http: ServerConfig = { 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Allow method spoofing 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Method spoofing enables defining custom HTTP methods using a query string 48 | | `_method`. This is usually required when you are making traditional 49 | | form requests and wants to use HTTP verbs like `PUT`, `DELETE` and 50 | | so on. 51 | | 52 | */ 53 | allowMethodSpoofing: false, 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Subdomain offset 58 | |-------------------------------------------------------------------------- 59 | */ 60 | subdomainOffset: 2, 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Request Ids 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Setting this value to `true` will generate a unique request id for each 68 | | HTTP request and set it as `x-request-id` header. 69 | | 70 | */ 71 | generateRequestId: false, 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Trusting proxy servers 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Define the proxy servers that AdonisJs must trust for reading `X-Forwarded` 79 | | headers. 80 | | 81 | */ 82 | trustProxy: proxyAddr.compile('loopback'), 83 | 84 | /* 85 | |-------------------------------------------------------------------------- 86 | | Generating Etag 87 | |-------------------------------------------------------------------------- 88 | | 89 | | Whether or not to generate an etag for every response. 90 | | 91 | */ 92 | etag: false, 93 | 94 | /* 95 | |-------------------------------------------------------------------------- 96 | | JSONP Callback 97 | |-------------------------------------------------------------------------- 98 | */ 99 | jsonpCallbackName: 'callback', 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Cookie settings 104 | |-------------------------------------------------------------------------- 105 | */ 106 | cookie: { 107 | domain: '', 108 | path: '/', 109 | maxAge: '2h', 110 | httpOnly: true, 111 | secure: false, 112 | sameSite: false, 113 | }, 114 | } 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Logger 119 | |-------------------------------------------------------------------------- 120 | */ 121 | export const logger: LoggerConfig = { 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | Application name 125 | |-------------------------------------------------------------------------- 126 | | 127 | | The name of the application you want to add to the log. It is recommended 128 | | to always have app name in every log line. 129 | | 130 | | The `APP_NAME` environment variable is automatically set by AdonisJS by 131 | | reading the `name` property from the `package.json` file. 132 | | 133 | */ 134 | name: Env.get('APP_NAME'), 135 | 136 | /* 137 | |-------------------------------------------------------------------------- 138 | | Toggle logger 139 | |-------------------------------------------------------------------------- 140 | | 141 | | Enable or disable logger application wide 142 | | 143 | */ 144 | enabled: true, 145 | 146 | /* 147 | |-------------------------------------------------------------------------- 148 | | Logging level 149 | |-------------------------------------------------------------------------- 150 | | 151 | | The level from which you want the logger to flush logs. It is recommended 152 | | to make use of the environment variable, so that you can define log levels 153 | | at deployment level and not code level. 154 | | 155 | */ 156 | level: Env.get('LOG_LEVEL', 'info'), 157 | 158 | /* 159 | |-------------------------------------------------------------------------- 160 | | Pretty print 161 | |-------------------------------------------------------------------------- 162 | | 163 | | It is highly advised NOT to use `prettyPrint` in production, since it 164 | | can have huge impact on performance. 165 | | 166 | */ 167 | prettyPrint: Env.get('NODE_ENV') === 'development', 168 | } 169 | 170 | /* 171 | |-------------------------------------------------------------------------- 172 | | Profiler 173 | |-------------------------------------------------------------------------- 174 | */ 175 | export const profiler: ProfilerConfig = { 176 | /* 177 | |-------------------------------------------------------------------------- 178 | | Toggle profiler 179 | |-------------------------------------------------------------------------- 180 | | 181 | | Enable or disable profiler 182 | | 183 | */ 184 | enabled: true, 185 | 186 | /* 187 | |-------------------------------------------------------------------------- 188 | | Blacklist actions/row labels 189 | |-------------------------------------------------------------------------- 190 | | 191 | | Define an array of actions or row labels that you want to disable from 192 | | getting profiled. 193 | | 194 | */ 195 | blacklist: [], 196 | 197 | /* 198 | |-------------------------------------------------------------------------- 199 | | Whitelist actions/row labels 200 | |-------------------------------------------------------------------------- 201 | | 202 | | Define an array of actions or row labels that you want to whitelist for 203 | | the profiler. When whitelist is defined, then `blacklist` is ignored. 204 | | 205 | */ 206 | whitelist: [], 207 | } 208 | 209 | /* 210 | |-------------------------------------------------------------------------- 211 | | Validator 212 | |-------------------------------------------------------------------------- 213 | | 214 | | Configure the global configuration for the validator. Here's the reference 215 | | to the default config https://git.io/JT0WE 216 | | 217 | */ 218 | export const validator: ValidatorConfig = {} 219 | 220 | /* 221 | |-------------------------------------------------------------------------- 222 | | Assets 223 | |-------------------------------------------------------------------------- 224 | | 225 | | Configure the asset manager you are using to compile the frontend assets 226 | | 227 | */ 228 | export const assets: AssetsManagerConfig = { 229 | /* 230 | |-------------------------------------------------------------------------- 231 | | Driver 232 | |-------------------------------------------------------------------------- 233 | | 234 | | Currently we only support webpack encore and may introduce more drivers 235 | | in the future 236 | | 237 | */ 238 | driver: Env.get('ASSETS_DRIVER'), 239 | 240 | /* 241 | |-------------------------------------------------------------------------- 242 | | Public path 243 | |-------------------------------------------------------------------------- 244 | | 245 | | Directory to search for the "manifest.json" and the "entrypoints.json" 246 | | files 247 | | 248 | */ 249 | publicPath: Application.publicPath('assets'), 250 | 251 | /* 252 | |-------------------------------------------------------------------------- 253 | | Script tag 254 | |-------------------------------------------------------------------------- 255 | | 256 | | Define attributes for the entryPointScripts tags 257 | | 258 | */ 259 | script: { 260 | attributes: { 261 | defer: true, 262 | }, 263 | }, 264 | 265 | /* 266 | |-------------------------------------------------------------------------- 267 | | Style tag 268 | |-------------------------------------------------------------------------- 269 | | 270 | | Define attributes for the entryPointStyles tags 271 | | 272 | */ 273 | style: { 274 | attributes: {}, 275 | }, 276 | } 277 | -------------------------------------------------------------------------------- /config/bodyparser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jfefn 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import type { BodyParserConfig } from '@ioc:Adonis/Core/BodyParser' 9 | 10 | const bodyParserConfig: BodyParserConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | White listed methods 14 | |-------------------------------------------------------------------------- 15 | | 16 | | HTTP methods for which body parsing must be performed. It is a good practice 17 | | to avoid body parsing for `GET` requests. 18 | | 19 | */ 20 | whitelistedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'], 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | JSON parser settings 25 | |-------------------------------------------------------------------------- 26 | | 27 | | The settings for the JSON parser. The types defines the request content 28 | | types which gets processed by the JSON parser. 29 | | 30 | */ 31 | json: { 32 | encoding: 'utf-8', 33 | limit: '1mb', 34 | strict: true, 35 | types: [ 36 | 'application/json', 37 | 'application/json-patch+json', 38 | 'application/vnd.api+json', 39 | 'application/csp-report', 40 | ], 41 | }, 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Form parser settings 46 | |-------------------------------------------------------------------------- 47 | | 48 | | The settings for the `application/x-www-form-urlencoded` parser. The types 49 | | defines the request content types which gets processed by the form parser. 50 | | 51 | */ 52 | form: { 53 | encoding: 'utf-8', 54 | limit: '1mb', 55 | queryString: {}, 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Convert empty strings to null 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Convert empty form fields to null. HTML forms results in field string 63 | | value when the field is left blank. This option normalizes all the blank 64 | | field values to "null" 65 | | 66 | */ 67 | convertEmptyStringsToNull: true, 68 | 69 | types: ['application/x-www-form-urlencoded'], 70 | }, 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Raw body parser settings 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Raw body just reads the request body stream as a plain text, which you 78 | | can process by hand. This must be used when request body type is not 79 | | supported by the body parser. 80 | | 81 | */ 82 | raw: { 83 | encoding: 'utf-8', 84 | limit: '1mb', 85 | queryString: {}, 86 | types: ['text/*'], 87 | }, 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Multipart parser settings 92 | |-------------------------------------------------------------------------- 93 | | 94 | | The settings for the `multipart/form-data` parser. The types defines the 95 | | request content types which gets processed by the form parser. 96 | | 97 | */ 98 | multipart: { 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Auto process 102 | |-------------------------------------------------------------------------- 103 | | 104 | | The auto process option will process uploaded files and writes them to 105 | | the `tmp` folder. You can turn it off and then manually use the stream 106 | | to pipe stream to a different destination. 107 | | 108 | | It is recommended to keep `autoProcess=true`. Unless you are processing bigger 109 | | file sizes. 110 | | 111 | */ 112 | autoProcess: true, 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Files to be processed manually 117 | |-------------------------------------------------------------------------- 118 | | 119 | | You can turn off `autoProcess` for certain routes by defining 120 | | routes inside the following array. 121 | | 122 | | NOTE: Make sure the route pattern starts with a leading slash. 123 | | 124 | | Correct 125 | | ```js 126 | | /projects/:id/file 127 | | ``` 128 | | 129 | | Incorrect 130 | | ```js 131 | | projects/:id/file 132 | | ``` 133 | */ 134 | processManually: [], 135 | 136 | /* 137 | |-------------------------------------------------------------------------- 138 | | Temporary file name 139 | |-------------------------------------------------------------------------- 140 | | 141 | | When auto processing is on. We will use this method to compute the temporary 142 | | file name. AdonisJs will compute a unique `tmpPath` for you automatically, 143 | | However, you can also define your own custom method. 144 | | 145 | */ 146 | // tmpFileName () { 147 | // }, 148 | 149 | /* 150 | |-------------------------------------------------------------------------- 151 | | Encoding 152 | |-------------------------------------------------------------------------- 153 | | 154 | | Request body encoding 155 | | 156 | */ 157 | encoding: 'utf-8', 158 | 159 | /* 160 | |-------------------------------------------------------------------------- 161 | | Convert empty strings to null 162 | |-------------------------------------------------------------------------- 163 | | 164 | | Convert empty form fields to null. HTML forms results in field string 165 | | value when the field is left blank. This option normalizes all the blank 166 | | field values to "null" 167 | | 168 | */ 169 | convertEmptyStringsToNull: true, 170 | 171 | /* 172 | |-------------------------------------------------------------------------- 173 | | Max Fields 174 | |-------------------------------------------------------------------------- 175 | | 176 | | The maximum number of fields allowed in the request body. The field includes 177 | | text inputs and files both. 178 | | 179 | */ 180 | maxFields: 1000, 181 | 182 | /* 183 | |-------------------------------------------------------------------------- 184 | | Request body limit 185 | |-------------------------------------------------------------------------- 186 | | 187 | | The total limit to the multipart body. This includes all request files 188 | | and fields data. 189 | | 190 | */ 191 | limit: '20mb', 192 | 193 | /* 194 | |-------------------------------------------------------------------------- 195 | | Types 196 | |-------------------------------------------------------------------------- 197 | | 198 | | The types that will be considered and parsed as multipart body. 199 | | 200 | */ 201 | types: ['multipart/form-data'], 202 | }, 203 | } 204 | 205 | export default bodyParserConfig 206 | -------------------------------------------------------------------------------- /config/cors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefC 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import type { CorsConfig } from '@ioc:Adonis/Core/Cors' 9 | 10 | const corsConfig: CorsConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Enabled 14 | |-------------------------------------------------------------------------- 15 | | 16 | | A boolean to enable or disable CORS integration from your AdonisJs 17 | | application. 18 | | 19 | | Setting the value to `true` will enable the CORS for all HTTP request. However, 20 | | you can define a function to enable/disable it on per request basis as well. 21 | | 22 | */ 23 | enabled: false, 24 | 25 | // You can also use a function that return true or false. 26 | // enabled: (request) => request.url().startsWith('/api') 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Origin 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Set a list of origins to be allowed for `Access-Control-Allow-Origin`. 34 | | The value can be one of the following: 35 | | 36 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 37 | | 38 | | Boolean (true) - Allow current request origin. 39 | | Boolean (false) - Disallow all. 40 | | String - Comma separated list of allowed origins. 41 | | Array - An array of allowed origins. 42 | | String (*) - A wildcard (*) to allow all request origins. 43 | | Function - Receives the current origin string and should return 44 | | one of the above values. 45 | | 46 | */ 47 | origin: true, 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Methods 52 | |-------------------------------------------------------------------------- 53 | | 54 | | An array of allowed HTTP methods for CORS. The `Access-Control-Request-Method` 55 | | is checked against the following list. 56 | | 57 | | Following is the list of default methods. Feel free to add more. 58 | */ 59 | methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Headers 64 | |-------------------------------------------------------------------------- 65 | | 66 | | List of headers to be allowed for `Access-Control-Allow-Headers` header. 67 | | The value can be one of the following: 68 | | 69 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers 70 | | 71 | | Boolean(true) - Allow all headers mentioned in `Access-Control-Request-Headers`. 72 | | Boolean(false) - Disallow all headers. 73 | | String - Comma separated list of allowed headers. 74 | | Array - An array of allowed headers. 75 | | Function - Receives the current header and should return one of the above values. 76 | | 77 | */ 78 | headers: true, 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Expose Headers 83 | |-------------------------------------------------------------------------- 84 | | 85 | | A list of headers to be exposed by setting `Access-Control-Expose-Headers`. 86 | | header. By default following 6 simple response headers are exposed. 87 | | 88 | | Cache-Control 89 | | Content-Language 90 | | Content-Type 91 | | Expires 92 | | Last-Modified 93 | | Pragma 94 | | 95 | | In order to add more headers, simply define them inside the following array. 96 | | 97 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers 98 | | 99 | */ 100 | exposeHeaders: [ 101 | 'cache-control', 102 | 'content-language', 103 | 'content-type', 104 | 'expires', 105 | 'last-modified', 106 | 'pragma', 107 | ], 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | Credentials 112 | |-------------------------------------------------------------------------- 113 | | 114 | | Toggle `Access-Control-Allow-Credentials` header. If value is set to `true`, 115 | | then header will be set, otherwise not. 116 | | 117 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials 118 | | 119 | */ 120 | credentials: true, 121 | 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | MaxAge 125 | |-------------------------------------------------------------------------- 126 | | 127 | | Define `Access-Control-Max-Age` header in seconds. 128 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age 129 | | 130 | */ 131 | maxAge: 90, 132 | } 133 | 134 | export default corsConfig 135 | -------------------------------------------------------------------------------- /config/drive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JBt3o 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { driveConfig } from '@adonisjs/core/build/config' 10 | import Application from '@ioc:Adonis/Core/Application' 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Drive Config 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The `DriveConfig` relies on the `DisksList` interface which is 18 | | defined inside the `contracts` directory. 19 | | 20 | */ 21 | export default driveConfig({ 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Default disk 25 | |-------------------------------------------------------------------------- 26 | | 27 | | The default disk to use for managing file uploads. The value is driven by 28 | | the `DRIVE_DISK` environment variable. 29 | | 30 | */ 31 | disk: Env.get('DRIVE_DISK'), 32 | 33 | disks: { 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Local 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Uses the local file system to manage files. Make sure to turn off serving 40 | | files when not using this disk. 41 | | 42 | */ 43 | local: { 44 | driver: 'local', 45 | visibility: 'public', 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Storage root - Local driver only 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Define an absolute path to the storage directory from where to read the 53 | | files. 54 | | 55 | */ 56 | root: Application.tmpPath('uploads'), 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Serve files - Local driver only 61 | |-------------------------------------------------------------------------- 62 | | 63 | | When this is set to true, AdonisJS will configure a files server to serve 64 | | files from the disk root. This is done to mimic the behavior of cloud 65 | | storage services that has inbuilt capabilities to serve files. 66 | | 67 | */ 68 | serveFiles: true, 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Base path - Local driver only 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Base path is always required when "serveFiles = true". Also make sure 76 | | the `basePath` is unique across all the disks using "local" driver and 77 | | you are not registering routes with this prefix. 78 | | 79 | */ 80 | basePath: '/uploads', 81 | }, 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | S3 Driver 86 | |-------------------------------------------------------------------------- 87 | | 88 | | Uses the S3 cloud storage to manage files. Make sure to install the s3 89 | | drive separately when using it. 90 | | 91 | |************************************************************************** 92 | | npm i @adonisjs/drive-s3 93 | |************************************************************************** 94 | | 95 | */ 96 | // s3: { 97 | // driver: 's3', 98 | // visibility: 'public', 99 | // key: Env.get('S3_KEY'), 100 | // secret: Env.get('S3_SECRET'), 101 | // region: Env.get('S3_REGION'), 102 | // bucket: Env.get('S3_BUCKET'), 103 | // endpoint: Env.get('S3_ENDPOINT'), 104 | // 105 | // // For minio to work 106 | // // forcePathStyle: true, 107 | // }, 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | GCS Driver 112 | |-------------------------------------------------------------------------- 113 | | 114 | | Uses the Google cloud storage to manage files. Make sure to install the GCS 115 | | drive separately when using it. 116 | | 117 | |************************************************************************** 118 | | npm i @adonisjs/drive-gcs 119 | |************************************************************************** 120 | | 121 | */ 122 | // gcs: { 123 | // driver: 'gcs', 124 | // visibility: 'public', 125 | // keyFilename: Env.get('GCS_KEY_FILENAME'), 126 | // bucket: Env.get('GCS_BUCKET'), 127 | 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | Uniform ACL - Google cloud storage only 131 | |-------------------------------------------------------------------------- 132 | | 133 | | When using the Uniform ACL on the bucket, the "visibility" option is 134 | | ignored. Since, the files ACL is managed by the google bucket policies 135 | | directly. 136 | | 137 | |************************************************************************** 138 | | Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access 139 | |************************************************************************** 140 | | 141 | | The following option just informs drive whether your bucket is using uniform 142 | | ACL or not. The actual setting needs to be toggled within the Google cloud 143 | | console. 144 | | 145 | */ 146 | // usingUniformAcl: false, 147 | // }, 148 | }, 149 | }) 150 | -------------------------------------------------------------------------------- /config/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JfefW 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { hashConfig } from '@adonisjs/core/build/config' 10 | 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Hash Config 14 | |-------------------------------------------------------------------------- 15 | | 16 | | The `HashConfig` relies on the `HashList` interface which is 17 | | defined inside `contracts` directory. 18 | | 19 | */ 20 | export default hashConfig({ 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Default hasher 24 | |-------------------------------------------------------------------------- 25 | | 26 | | By default we make use of the argon hasher to hash values. However, feel 27 | | free to change the default value 28 | | 29 | */ 30 | default: Env.get('HASH_DRIVER', 'scrypt'), 31 | 32 | list: { 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | scrypt 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Scrypt mapping uses the Node.js inbuilt crypto module for creating 39 | | hashes. 40 | | 41 | | We are using the default configuration recommended within the Node.js 42 | | documentation. 43 | | https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback 44 | | 45 | */ 46 | scrypt: { 47 | driver: 'scrypt', 48 | cost: 16384, 49 | blockSize: 8, 50 | parallelization: 1, 51 | saltSize: 16, 52 | keyLength: 64, 53 | maxMemory: 32 * 1024 * 1024, 54 | }, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Argon 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Argon mapping uses the `argon2` driver to hash values. 62 | | 63 | | Make sure you install the underlying dependency for this driver to work. 64 | | https://www.npmjs.com/package/phc-argon2. 65 | | 66 | | npm install phc-argon2 67 | | 68 | */ 69 | argon: { 70 | driver: 'argon2', 71 | variant: 'id', 72 | iterations: 3, 73 | memory: 4096, 74 | parallelism: 1, 75 | saltSize: 16, 76 | }, 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Bcrypt 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Bcrypt mapping uses the `bcrypt` driver to hash values. 84 | | 85 | | Make sure you install the underlying dependency for this driver to work. 86 | | https://www.npmjs.com/package/phc-bcrypt. 87 | | 88 | | npm install phc-bcrypt 89 | | 90 | */ 91 | bcrypt: { 92 | driver: 'bcrypt', 93 | rounds: 10, 94 | }, 95 | }, 96 | }) 97 | -------------------------------------------------------------------------------- /config/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JeYHp 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import Application from '@ioc:Adonis/Core/Application' 10 | import { sessionConfig } from '@adonisjs/session/build/config' 11 | 12 | export default sessionConfig({ 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Enable/Disable sessions 16 | |-------------------------------------------------------------------------- 17 | | 18 | | Setting the following property to "false" will disable the session for the 19 | | entire application 20 | | 21 | */ 22 | enabled: true, 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Driver 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The session driver to use. You can choose between one of the following 30 | | drivers. 31 | | 32 | | - cookie (Uses signed cookies to store session values) 33 | | - file (Uses filesystem to store session values) 34 | | - redis (Uses redis. Make sure to install "@adonisjs/redis" as well) 35 | | 36 | | Note: Switching drivers will make existing sessions invalid. 37 | | 38 | */ 39 | driver: Env.get('SESSION_DRIVER'), 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Cookie name 44 | |-------------------------------------------------------------------------- 45 | | 46 | | The name of the cookie that will hold the session id. 47 | | 48 | */ 49 | cookieName: 'adonis-session', 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Clear session when browser closes 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Whether or not you want to destroy the session when browser closes. Setting 57 | | this value to `true` will ignore the `age`. 58 | | 59 | */ 60 | clearWithBrowser: false, 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Session age 65 | |-------------------------------------------------------------------------- 66 | | 67 | | The duration for which session stays active after no activity. A new HTTP 68 | | request to the server is considered as activity. 69 | | 70 | | The value can be a number in milliseconds or a string that must be valid 71 | | as per https://npmjs.org/package/ms package. 72 | | 73 | | Example: `2 days`, `2.5 hrs`, `1y`, `5s` and so on. 74 | | 75 | */ 76 | age: '2h', 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Cookie values 81 | |-------------------------------------------------------------------------- 82 | | 83 | | The cookie settings are used to setup the session id cookie and also the 84 | | driver will use the same values. 85 | | 86 | */ 87 | cookie: { 88 | path: '/', 89 | httpOnly: true, 90 | sameSite: false, 91 | }, 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Configuration for the file driver 96 | |-------------------------------------------------------------------------- 97 | | 98 | | The file driver needs absolute path to the directory in which sessions 99 | | must be stored. 100 | | 101 | */ 102 | file: { 103 | location: Application.tmpPath('sessions'), 104 | }, 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Redis driver 109 | |-------------------------------------------------------------------------- 110 | | 111 | | The redis connection you want session driver to use. The same connection 112 | | must be defined inside `config/redis.ts` file as well. 113 | | 114 | */ 115 | redisConnection: 'local', 116 | }) 117 | -------------------------------------------------------------------------------- /config/shield.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jvwvt 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import Env from '@ioc:Adonis/Core/Env' 9 | import { ShieldConfig } from '@ioc:Adonis/Addons/Shield' 10 | 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Content Security Policy 14 | |-------------------------------------------------------------------------- 15 | | 16 | | Content security policy filters out the origins not allowed to execute 17 | | and load resources like scripts, styles and fonts. There are wide 18 | | variety of options to choose from. 19 | */ 20 | export const csp: ShieldConfig['csp'] = { 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Enable/disable CSP 24 | |-------------------------------------------------------------------------- 25 | | 26 | | The CSP rules are disabled by default for seamless onboarding. 27 | | 28 | */ 29 | enabled: false, 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Directives 34 | |-------------------------------------------------------------------------- 35 | | 36 | | All directives are defined in camelCase and here is the list of 37 | | available directives and their possible values. 38 | | 39 | | https://content-security-policy.com 40 | | 41 | | @example 42 | | directives: { 43 | | defaultSrc: ["'self'", '@nonce', 'cdnjs.cloudflare.com'] 44 | | } 45 | | 46 | */ 47 | directives: {}, 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Report only 52 | |-------------------------------------------------------------------------- 53 | | 54 | | Setting `reportOnly=true` will not block the scripts from running and 55 | | instead report them to a URL. 56 | | 57 | */ 58 | reportOnly: false, 59 | } 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | CSRF Protection 64 | |-------------------------------------------------------------------------- 65 | | 66 | | CSRF Protection adds another layer of security by making sure, actionable 67 | | routes does have a valid token to execute an action. 68 | | 69 | */ 70 | export const csrf: ShieldConfig['csrf'] = { 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Enable/Disable CSRF 74 | |-------------------------------------------------------------------------- 75 | */ 76 | enabled: true, 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Routes to Ignore 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Define an array of route patterns that you want to ignore from CSRF 84 | | validation. Make sure the route patterns are started with a leading 85 | | slash. Example: 86 | | 87 | | `/foo/bar` 88 | | 89 | | Also you can define a function that is evaluated on every HTTP Request. 90 | | ``` 91 | | exceptRoutes: ({ request }) => request.url().includes('/api') 92 | | ``` 93 | | 94 | */ 95 | exceptRoutes: [], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Enable Sharing Token Via Cookie 100 | |-------------------------------------------------------------------------- 101 | | 102 | | When the following flag is enabled, AdonisJS will drop `XSRF-TOKEN` 103 | | cookie that frontend frameworks can read and return back as a 104 | | `X-XSRF-TOKEN` header. 105 | | 106 | | The cookie has `httpOnly` flag set to false, so it is little insecure and 107 | | can be turned off when you are not using a frontend framework making 108 | | AJAX requests. 109 | | 110 | */ 111 | enableXsrfCookie: true, 112 | 113 | /* 114 | |-------------------------------------------------------------------------- 115 | | Methods to Validate 116 | |-------------------------------------------------------------------------- 117 | | 118 | | Define an array of HTTP methods to be validated for a valid CSRF token. 119 | | 120 | */ 121 | methods: ['POST', 'PUT', 'PATCH', 'DELETE'], 122 | } 123 | 124 | /* 125 | |-------------------------------------------------------------------------- 126 | | DNS Prefetching 127 | |-------------------------------------------------------------------------- 128 | | 129 | | DNS prefetching allows browsers to proactively perform domain name 130 | | resolution in background. 131 | | 132 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control 133 | | 134 | */ 135 | export const dnsPrefetch: ShieldConfig['dnsPrefetch'] = { 136 | /* 137 | |-------------------------------------------------------------------------- 138 | | Enable/disable this feature 139 | |-------------------------------------------------------------------------- 140 | */ 141 | enabled: true, 142 | 143 | /* 144 | |-------------------------------------------------------------------------- 145 | | Allow or Dis-Allow Explicitly 146 | |-------------------------------------------------------------------------- 147 | | 148 | | The `enabled` boolean does not set `X-DNS-Prefetch-Control` header. However 149 | | the `allow` boolean controls the value of `X-DNS-Prefetch-Control` header. 150 | | 151 | | - When `allow = true`, then `X-DNS-Prefetch-Control = 'on'` 152 | | - When `allow = false`, then `X-DNS-Prefetch-Control = 'off'` 153 | | 154 | */ 155 | allow: true, 156 | } 157 | 158 | /* 159 | |-------------------------------------------------------------------------- 160 | | Iframe Options 161 | |-------------------------------------------------------------------------- 162 | | 163 | | xFrame defines whether or not your website can be embedded inside an 164 | | iframe. Choose from one of the following options. 165 | | 166 | | - DENY 167 | | - SAMEORIGIN 168 | | - ALLOW-FROM http://example.com 169 | | 170 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 171 | */ 172 | export const xFrame: ShieldConfig['xFrame'] = { 173 | enabled: true, 174 | action: 'DENY', 175 | } 176 | 177 | /* 178 | |-------------------------------------------------------------------------- 179 | | Http Strict Transport Security 180 | |-------------------------------------------------------------------------- 181 | | 182 | | A security to ensure that a browser always makes a connection over 183 | | HTTPS. 184 | | 185 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 186 | | 187 | */ 188 | export const hsts: ShieldConfig['hsts'] = { 189 | enabled: true, 190 | /* 191 | |-------------------------------------------------------------------------- 192 | | Max Age 193 | |-------------------------------------------------------------------------- 194 | | 195 | | Control, how long the browser should remember that a site is only to be 196 | | accessed using HTTPS. 197 | | 198 | */ 199 | maxAge: '180 days', 200 | 201 | /* 202 | |-------------------------------------------------------------------------- 203 | | Include Subdomains 204 | |-------------------------------------------------------------------------- 205 | | 206 | | Apply rules on the subdomains as well. 207 | | 208 | */ 209 | includeSubDomains: true, 210 | 211 | /* 212 | |-------------------------------------------------------------------------- 213 | | Preloading 214 | |-------------------------------------------------------------------------- 215 | | 216 | | Google maintains a service to register your domain and it will preload 217 | | the HSTS policy. Learn more https://hstspreload.org/ 218 | | 219 | */ 220 | preload: false, 221 | } 222 | 223 | /* 224 | |-------------------------------------------------------------------------- 225 | | No Sniff 226 | |-------------------------------------------------------------------------- 227 | | 228 | | Browsers have a habit of sniffing content-type of a response. Which means 229 | | files with .txt extension containing Javascript code will be executed as 230 | | Javascript. You can disable this behavior by setting nosniff to false. 231 | | 232 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 233 | | 234 | */ 235 | export const contentTypeSniffing: ShieldConfig['contentTypeSniffing'] = { 236 | enabled: true, 237 | } 238 | -------------------------------------------------------------------------------- /config/static.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/Jfefl 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this config 5 | * file. 6 | */ 7 | 8 | import { AssetsConfig } from '@ioc:Adonis/Core/Static' 9 | 10 | const staticConfig: AssetsConfig = { 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Enabled 14 | |-------------------------------------------------------------------------- 15 | | 16 | | A boolean to enable or disable serving static files. The static files 17 | | are served from the `public` directory inside the application root. 18 | | However, you can override the default path inside `.adonisrc.json` 19 | | file. 20 | | 21 | | 22 | */ 23 | enabled: true, 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Handling Dot Files 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Decide how you want the static assets server to handle the `dotfiles`. 31 | | By default, we ignore them as if they don't exists. However, you 32 | | can choose between one of the following options. 33 | | 34 | | - ignore: Behave as if the file doesn't exists. Results in 404. 35 | | - deny: Deny access to the file. Results in 403. 36 | | - allow: Serve the file contents 37 | | 38 | */ 39 | dotFiles: 'ignore', 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Generating Etag 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Handle whether or not to generate etags for the files. Etag allows browser 47 | | to utilize the cache when file hasn't been changed. 48 | | 49 | */ 50 | etag: true, 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Set Last Modified 55 | |-------------------------------------------------------------------------- 56 | | 57 | | Whether or not to set the `Last-Modified` header in the response. Uses 58 | | the file system's last modified value. 59 | | 60 | */ 61 | lastModified: true, 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Max age 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Set the value for the max-age directive. Set a higher value in production 69 | | if you fingerprint your assets. 70 | | 71 | | Learn more: https://docs.adonisjs.com/guides/deployment#serving-static-assets 72 | | 73 | */ 74 | maxAge: 0, 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Immutable 79 | |-------------------------------------------------------------------------- 80 | | 81 | | Set the immutable directive. Set it to `true` if the assets are generated 82 | | with a fingerprint. In others words the file name changes when the file 83 | | contents change. 84 | | 85 | */ 86 | immutable: false, 87 | } 88 | 89 | export default staticConfig 90 | -------------------------------------------------------------------------------- /contracts/drive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JBt3I 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { InferDisksFromConfig } from '@adonisjs/core/build/config' 9 | import type driveConfig from '../config/drive' 10 | 11 | declare module '@ioc:Adonis/Core/Drive' { 12 | interface DisksList extends InferDisksFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JTm6U 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | declare module '@ioc:Adonis/Core/Env' { 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Getting types for validated environment variables 12 | |-------------------------------------------------------------------------- 13 | | 14 | | The `default` export from the "../env.ts" file exports types for the 15 | | validated environment variables. Here we merge them with the `EnvTypes` 16 | | interface so that you can enjoy intellisense when using the "Env" 17 | | module. 18 | | 19 | */ 20 | 21 | type CustomTypes = typeof import('../env').default 22 | interface EnvTypes extends CustomTypes {} 23 | } 24 | -------------------------------------------------------------------------------- /contracts/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/JfefG 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | declare module '@ioc:Adonis/Core/Event' { 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Define typed events 12 | |-------------------------------------------------------------------------- 13 | | 14 | | You can define types for events inside the following interface and 15 | | AdonisJS will make sure that all listeners and emit calls adheres 16 | | to the defined types. 17 | | 18 | | For example: 19 | | 20 | | interface EventsList { 21 | | 'new:user': UserModel 22 | | } 23 | | 24 | | Now calling `Event.emit('new:user')` will statically ensure that passed value is 25 | | an instance of the the UserModel only. 26 | | 27 | */ 28 | interface EventsList { 29 | // 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://git.io/Jfefs 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { InferListFromConfig } from '@adonisjs/core/build/config' 9 | import type hashConfig from '../config/hash' 10 | 11 | declare module '@ioc:Adonis/Core/Hash' { 12 | interface HashersList extends InferListFromConfig {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/tests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract source: https://bit.ly/3DP1ypf 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import '@japa/runner' 9 | 10 | declare module '@japa/runner' { 11 | interface TestContext { 12 | // Extend context 13 | } 14 | 15 | interface Test { 16 | // Extend test 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | adonis_app: 5 | container_name: adonis_app 6 | restart: always 7 | build: 8 | context: . 9 | target: dependencies 10 | ports: 11 | - ${PORT}:${PORT} 12 | - 9229:9229 13 | env_file: 14 | - .env 15 | volumes: 16 | - ./:/home/node/app 17 | # Uncomment the below line if you developing on MacOS 18 | #- /home/node/app/node_modules 19 | command: dumb-init node ace serve --watch --node-args="--inspect=0.0.0.0" 20 | -------------------------------------------------------------------------------- /env.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Validating Environment Variables 4 | |-------------------------------------------------------------------------- 5 | | 6 | | In this file we define the rules for validating environment variables. 7 | | By performing validation we ensure that your application is running in 8 | | a stable environment with correct configuration values. 9 | | 10 | | This file is read automatically by the framework during the boot lifecycle 11 | | and hence do not rename or move this file to a different location. 12 | | 13 | */ 14 | 15 | import Env from '@ioc:Adonis/Core/Env' 16 | 17 | export default Env.rules({ 18 | HOST: Env.schema.string({ format: 'host' }), 19 | PORT: Env.schema.number(), 20 | APP_KEY: Env.schema.string(), 21 | APP_NAME: Env.schema.string(), 22 | CACHE_VIEWS: Env.schema.boolean(), 23 | SESSION_DRIVER: Env.schema.string(), 24 | DRIVE_DISK: Env.schema.enum(['local'] as const), 25 | NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), 26 | PROXY_URL: Env.schema.string(), 27 | RELAYS: Env.schema.string(), 28 | }) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nostr-proxy", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "node ace serve --watch", 7 | "build": "node ace build --production --ignore-ts-errors", 8 | "start": "node server.js", 9 | "lint": "eslint . --ext=.ts", 10 | "format": "prettier --write ." 11 | }, 12 | "eslintConfig": { 13 | "extends": [ 14 | "plugin:adonis/typescriptApp", 15 | "prettier" 16 | ], 17 | "plugins": [ 18 | "prettier" 19 | ], 20 | "rules": { 21 | "prettier/prettier": [ 22 | "error" 23 | ] 24 | } 25 | }, 26 | "eslintIgnore": [ 27 | "build" 28 | ], 29 | "prettier": { 30 | "trailingComma": "es5", 31 | "semi": false, 32 | "singleQuote": true, 33 | "useTabs": false, 34 | "quoteProps": "consistent", 35 | "bracketSpacing": true, 36 | "arrowParens": "always", 37 | "printWidth": 100 38 | }, 39 | "devDependencies": { 40 | "@adonisjs/assembler": "^5.9.5", 41 | "@babel/core": "^7.17.0", 42 | "@babel/preset-env": "^7.16.0", 43 | "@japa/preset-adonis": "^1.2.0", 44 | "@japa/runner": "^2.3.0", 45 | "@symfony/webpack-encore": "4.1.1", 46 | "@types/proxy-addr": "^2.0.0", 47 | "@types/source-map-support": "^0.5.6", 48 | "@types/ws": "^8.5.4", 49 | "adonis-preset-ts": "^2.1.0", 50 | "eslint": "^8.34.0", 51 | "eslint-config-prettier": "^8.6.0", 52 | "eslint-plugin-adonis": "^2.1.1", 53 | "eslint-plugin-prettier": "^4.2.1", 54 | "pino-pretty": "^9.2.0", 55 | "prettier": "^2.8.4", 56 | "typescript": "~4.6", 57 | "webpack": "^5.72", 58 | "webpack-cli": "^4.9.1", 59 | "youch": "^3.2.3", 60 | "youch-terminal": "^2.2.0" 61 | }, 62 | "dependencies": { 63 | "@adonisjs/core": "^5.8.0", 64 | "@adonisjs/repl": "^3.1.0", 65 | "@adonisjs/session": "^6.2.0", 66 | "@adonisjs/shield": "^7.0.0", 67 | "@adonisjs/view": "^6.1.0", 68 | "@dolu/nostr-tools": "1.5.5", 69 | "@noble/hashes": "^1.2.0", 70 | "@noble/secp256k1": "^1.7.1", 71 | "@types/uuid": "^9.0.0", 72 | "node-cron": "^3.0.2", 73 | "nostr-relaypool": "^0.4.17", 74 | "nostr-tools": "^1.4.1", 75 | "proxy-addr": "^2.0.7", 76 | "reflect-metadata": "^0.1.13", 77 | "sha256": "link:@noble/hashes/sha256", 78 | "source-map-support": "^0.5.21", 79 | "uuid": "^9.0.0", 80 | "ws": "^8.12.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /providers/AppProvider.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | 3 | export default class AppProvider { 4 | constructor(protected app: ApplicationContract) { } 5 | 6 | public register() { 7 | // Register your own bindings 8 | } 9 | 10 | public async boot() { 11 | // IoC container is ready 12 | } 13 | 14 | public async ready() { 15 | // App is ready 16 | if (this.app.environment === 'web') { 17 | await import('../start/socket') 18 | // await import('../start/cron') 19 | } 20 | } 21 | 22 | public async shutdown() { 23 | // Cleanup, since app is going down 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/assets/entrypoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "entrypoints": { 3 | "app": { 4 | "css": [ 5 | "http://localhost:8080/assets/app.css" 6 | ], 7 | "js": [ 8 | "http://localhost:8080/assets/app.js" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /public/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets/app.css": "http://localhost:8080/assets/app.css", 3 | "assets/app.js": "http://localhost:8080/assets/app.js" 4 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dolu89/nostr-proxy/26d6d96d5bcb9a7f9e498f292881beb997199dbc/public/favicon.ico -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | color: #666; 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Inter', sans-serif; 7 | } 8 | 9 | body { 10 | height: 100vh; 11 | height: 100dvh; 12 | display: grid; 13 | grid-template-rows: 1fr min-content; 14 | } 15 | 16 | a { 17 | color: #ce9ffc; 18 | text-decoration: none; 19 | } 20 | 21 | a:hover { 22 | color: #7367f0; 23 | } 24 | 25 | .container { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | justify-content: space-between; 30 | width: 100%; 31 | height: 100%; 32 | overflow-y: auto; 33 | } 34 | 35 | .header { 36 | display: flex; 37 | flex-direction: column; 38 | align-items: start; 39 | justify-content: center; 40 | margin-bottom: 50px; 41 | } 42 | 43 | @media (min-width: 421px) { 44 | .header { 45 | align-items: end; 46 | } 47 | } 48 | 49 | .header > .title { 50 | font-size: 4.5rem; 51 | font-weight: 900; 52 | margin: 0; 53 | } 54 | 55 | .header > .title > .gradient { 56 | background: -webkit-linear-gradient(45deg, #ce9ffc, #7367f0); 57 | -webkit-background-clip: text; 58 | -webkit-text-fill-color: transparent; 59 | } 60 | 61 | .content { 62 | margin-top: 60px; 63 | } 64 | 65 | .copy-text { 66 | width: 100%; 67 | box-sizing: border-box; 68 | padding: 10px; 69 | font-size: 1.2rem; 70 | font-weight: 500; 71 | color: #000; 72 | background: linear-gradient(white, white) padding-box, 73 | linear-gradient(to right, #ce9ffc, #7367f0) border-box; 74 | border-radius: 4px; 75 | border: 3px solid transparent; 76 | text-align: center; 77 | margin-bottom: 30px; 78 | } 79 | 80 | .footer { 81 | padding: 10px 30px; 82 | bottom: 0; 83 | left: 0; 84 | right: 0; 85 | display: flex; 86 | flex-direction: column; 87 | gap: 10px; 88 | align-items: baseline; 89 | background: #000; 90 | color: #fff; 91 | } 92 | 93 | @media (min-width: 480px) { 94 | .footer { 95 | flex-direction: row; 96 | justify-content: end; 97 | gap: 30px; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import '../css/app.css' 2 | -------------------------------------------------------------------------------- /resources/views/errors/not-found.edge: -------------------------------------------------------------------------------- 1 |

It's a 404

2 | -------------------------------------------------------------------------------- /resources/views/errors/server-error.edge: -------------------------------------------------------------------------------- 1 |

It's a 500

2 | -------------------------------------------------------------------------------- /resources/views/errors/unauthorized.edge: -------------------------------------------------------------------------------- 1 |

It's a 403

2 | -------------------------------------------------------------------------------- /resources/views/welcome.edge: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Nostr Proxy 12 | @entryPointStyles('app') 13 | @entryPointScripts('app') 14 | 15 | 16 | 17 |
18 |
19 | 20 |
21 |

Nostr Proxy

22 | Using this proxy, you save {{ relaysCount }} WS connections. 23 |
24 | 25 | 26 | 27 | 28 | 🟢 Connected {{relays.filter(relay => relay.connected).length}}/{{relaysCount}} 29 |
    30 | @each(relay in relays.filter(relay => relay.connected)) 31 |
  • {{ relay.url }}
  • 32 | @end 33 |
34 | 35 | 🔴 Disconnected 36 |
    37 | @each(relay in relays.filter(relay => !relay.connected)) 38 |
  • {{ relay.url }}
  • 39 | @end 40 |
41 |
42 | 43 |
44 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | AdonisJs Server 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file is meant to bootstrap the AdonisJs application 7 | | and start the HTTP server to accept incoming connections. You must avoid 8 | | making this file dirty and instead make use of `lifecycle hooks` provided 9 | | by AdonisJs service providers for custom code. 10 | | 11 | */ 12 | 13 | import 'reflect-metadata' 14 | import sourceMapSupport from 'source-map-support' 15 | import { Ignitor } from '@adonisjs/core/build/standalone' 16 | 17 | sourceMapSupport.install({ handleUncaughtExceptions: false }) 18 | 19 | new Ignitor(__dirname).httpServer().start() 20 | -------------------------------------------------------------------------------- /start/cron.ts: -------------------------------------------------------------------------------- 1 | // import cron from "node-cron"; 2 | // import WebSocketHandler from "../app/Services/WebSocketHandler"; 3 | 4 | // cron.schedule('*/5 * * * *', async () => { 5 | // if (!WebSocketHandler.booted) return 6 | // await WebSocketHandler.initRelays() 7 | // }); -------------------------------------------------------------------------------- /start/kernel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Application middleware 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is used to define middleware for HTTP requests. You can register 7 | | middleware as a `closure` or an IoC container binding. The bindings are 8 | | preferred, since they keep this file clean. 9 | | 10 | */ 11 | 12 | import Server from '@ioc:Adonis/Core/Server' 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Global middleware 17 | |-------------------------------------------------------------------------- 18 | | 19 | | An array of global middleware, that will be executed in the order they 20 | | are defined for every HTTP requests. 21 | | 22 | */ 23 | Server.middleware.register([() => import('@ioc:Adonis/Core/BodyParser')]) 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Named middleware 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Named middleware are defined as key-value pair. The value is the namespace 31 | | or middleware function and key is the alias. Later you can use these 32 | | alias on individual routes. For example: 33 | | 34 | | { auth: () => import('App/Middleware/Auth') } 35 | | 36 | | and then use it as follows 37 | | 38 | | Route.get('dashboard', 'UserController.dashboard').middleware('auth') 39 | | 40 | */ 41 | Server.middleware.registerNamed({}) 42 | -------------------------------------------------------------------------------- /start/routes.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ioc:Adonis/Core/Route' 2 | import Env from "@ioc:Adonis/Core/Env" 3 | import NostrSocket from '../app/Services/WebSocketHandler' 4 | import WebSocketHandler from '../app/Services/WebSocketHandler' 5 | // import NostrPool from '../app/Services/NostrPool' 6 | 7 | Route.get('/', async ({ view }) => { 8 | if (!NostrSocket.booted) return { message: 'Nostr Proxy is booting...' } 9 | 10 | const proxyUrl = Env.get('PROXY_URL') 11 | const relays = await WebSocketHandler.getRelays() 12 | const relaysCount = relays.length 13 | return view.render('welcome', { proxyUrl, relays, relaysCount }) 14 | }) 15 | 16 | Route.get('/stats', async () => { 17 | if (!NostrSocket.booted) return { message: 'Nostr Proxy is booting...' } 18 | 19 | const stats = NostrSocket.getStats() 20 | return stats 21 | }) -------------------------------------------------------------------------------- /start/socket.ts: -------------------------------------------------------------------------------- 1 | import WebSocketInstance from "../app/Services/WebSocketInstance" 2 | 3 | // Boot Websocket server 4 | WebSocketInstance.boot() -------------------------------------------------------------------------------- /stress-test.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | 3 | // Shell command: 4 | // for i in {1..10}; node stress-test.js; 5 | 6 | const socket = new WebSocket('wss://nproxy.cc') 7 | socket.on('open', () => { 8 | socket.send( 9 | '["REQ","feed:global",{"kinds":[1],"limit":100}]' 10 | ) 11 | }) 12 | socket.on('message', (data) => { 13 | console.log(data.toString()) 14 | }) 15 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Tests 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file boots the AdonisJS application and configures 7 | | the Japa tests runner. 8 | | 9 | | For the most part you will never edit this file. The configuration 10 | | for the tests can be controlled via ".adonisrc.json" and 11 | | "tests/bootstrap.ts" files. 12 | | 13 | */ 14 | 15 | process.env.NODE_ENV = 'test' 16 | 17 | import 'reflect-metadata' 18 | import sourceMapSupport from 'source-map-support' 19 | import { Ignitor } from '@adonisjs/core/build/standalone' 20 | import { configure, processCliArgs, run, RunnerHooksHandler } from '@japa/runner' 21 | 22 | sourceMapSupport.install({ handleUncaughtExceptions: false }) 23 | 24 | const kernel = new Ignitor(__dirname).kernel('test') 25 | 26 | kernel 27 | .boot() 28 | .then(() => import('./tests/bootstrap')) 29 | .then(({ runnerHooks, ...config }) => { 30 | const app: RunnerHooksHandler[] = [() => kernel.start()] 31 | 32 | configure({ 33 | ...kernel.application.rcFile.tests, 34 | ...processCliArgs(process.argv.slice(2)), 35 | ...config, 36 | ...{ 37 | importer: (filePath) => import(filePath), 38 | setup: app.concat(runnerHooks.setup), 39 | teardown: runnerHooks.teardown, 40 | }, 41 | cwd: kernel.application.appRoot, 42 | }) 43 | 44 | run() 45 | }) 46 | -------------------------------------------------------------------------------- /tests/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File source: https://bit.ly/3ukaHTz 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { Config } from '@japa/runner' 9 | import TestUtils from '@ioc:Adonis/Core/TestUtils' 10 | import { assert, runFailedTests, specReporter, apiClient } from '@japa/preset-adonis' 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Japa Plugins 15 | |-------------------------------------------------------------------------- 16 | | 17 | | Japa plugins allows you to add additional features to Japa. By default 18 | | we register the assertion plugin. 19 | | 20 | | Feel free to remove existing plugins or add more. 21 | | 22 | */ 23 | export const plugins: Required['plugins'] = [assert(), runFailedTests(), apiClient()] 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Japa Reporters 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Japa reporters displays/saves the progress of tests as they are executed. 31 | | By default, we register the spec reporter to show a detailed report 32 | | of tests on the terminal. 33 | | 34 | */ 35 | export const reporters: Required['reporters'] = [specReporter()] 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Runner hooks 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Runner hooks are executed after booting the AdonisJS app and 43 | | before the test files are imported. 44 | | 45 | | You can perform actions like starting the HTTP server or running migrations 46 | | within the runner hooks 47 | | 48 | */ 49 | export const runnerHooks: Pick, 'setup' | 'teardown'> = { 50 | setup: [() => TestUtils.ace().loadCommands()], 51 | teardown: [], 52 | } 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Configure individual suites 57 | |-------------------------------------------------------------------------- 58 | | 59 | | The configureSuite method gets called for every test suite registered 60 | | within ".adonisrc.json" file. 61 | | 62 | | You can use this method to configure suites. For example: Only start 63 | | the HTTP server when it is a functional suite. 64 | */ 65 | export const configureSuite: Required['configureSuite'] = (suite) => { 66 | if (suite.name === 'functional') { 67 | suite.setup(() => TestUtils.httpServer().start()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/functional/hello_world.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@japa/runner' 2 | 3 | test('display welcome page', async ({ client }) => { 4 | const response = await client.get('/') 5 | 6 | response.assertStatus(200) 7 | response.assertTextIncludes('

It Works!

') 8 | }) 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "adonis-preset-ts/tsconfig.json", 3 | "include": [ 4 | "**/*" 5 | ], 6 | "exclude": [ 7 | "node_modules", 8 | "build" 9 | ], 10 | "compilerOptions": { 11 | "outDir": "build", 12 | "rootDir": "./", 13 | "sourceMap": true, 14 | "paths": { 15 | "App/*": [ 16 | "./app/*" 17 | ], 18 | "Config/*": [ 19 | "./config/*" 20 | ], 21 | "Contracts/*": [ 22 | "./contracts/*" 23 | ], 24 | "Database/*": [ 25 | "./database/*" 26 | ] 27 | }, 28 | "types": [ 29 | "@adonisjs/core", 30 | "@adonisjs/repl", 31 | "@adonisjs/session", 32 | "@adonisjs/view", 33 | "@adonisjs/shield", 34 | "@japa/preset-adonis/build/adonis-typings", 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const Encore = require('@symfony/webpack-encore') 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Encore runtime environment 7 | |-------------------------------------------------------------------------- 8 | */ 9 | if (!Encore.isRuntimeEnvironmentConfigured()) { 10 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev') 11 | } 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Output path 16 | |-------------------------------------------------------------------------- 17 | | 18 | | The output path for writing the compiled files. It should always 19 | | be inside the public directory, so that AdonisJS can serve it. 20 | | 21 | */ 22 | Encore.setOutputPath('./public/assets') 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Public URI 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The public URI to access the static files. It should always be 30 | | relative from the "public" directory. 31 | | 32 | */ 33 | Encore.setPublicPath('/assets') 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Entrypoints 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Entrypoints are script files that boots your frontend application. Ideally 41 | | a single entrypoint is used by majority of applications. However, feel 42 | | free to add more (if required). 43 | | 44 | | Also, make sure to read the docs on "Assets bundler" to learn more about 45 | | entrypoints. 46 | | 47 | */ 48 | Encore.addEntry('app', './resources/js/app.js') 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Copy assets 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Since the edge templates are not part of the Webpack compile lifecycle, any 56 | | images referenced by it will not be processed by Webpack automatically. Hence 57 | | we must copy them manually. 58 | | 59 | */ 60 | // Encore.copyFiles({ 61 | // from: './resources/images', 62 | // to: 'images/[path][name].[hash:8].[ext]', 63 | // }) 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Split shared code 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Instead of bundling duplicate code in all the bundles, generate a separate 71 | | bundle for the shared code. 72 | | 73 | | https://symfony.com/doc/current/frontend/encore/split-chunks.html 74 | | https://webpack.js.org/plugins/split-chunks-plugin/ 75 | | 76 | */ 77 | // Encore.splitEntryChunks() 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Isolated entrypoints 82 | |-------------------------------------------------------------------------- 83 | | 84 | | Treat each entry point and its dependencies as its own isolated module. 85 | | 86 | */ 87 | Encore.disableSingleRuntimeChunk() 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Cleanup output folder 92 | |-------------------------------------------------------------------------- 93 | | 94 | | It is always nice to cleanup the build output before creating a build. It 95 | | will ensure that all unused files from the previous build are removed. 96 | | 97 | */ 98 | Encore.cleanupOutputBeforeBuild() 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Source maps 103 | |-------------------------------------------------------------------------- 104 | | 105 | | Enable source maps in production 106 | | 107 | */ 108 | Encore.enableSourceMaps(!Encore.isProduction()) 109 | 110 | /* 111 | |-------------------------------------------------------------------------- 112 | | Assets versioning 113 | |-------------------------------------------------------------------------- 114 | | 115 | | Enable assets versioning to leverage lifetime browser and CDN cache 116 | | 117 | */ 118 | Encore.enableVersioning(Encore.isProduction()) 119 | 120 | /* 121 | |-------------------------------------------------------------------------- 122 | | Configure dev server 123 | |-------------------------------------------------------------------------- 124 | | 125 | | Here we configure the dev server to enable live reloading for edge templates. 126 | | Remember edge templates are not processed by Webpack and hence we need 127 | | to watch them explicitly and livereload the browser. 128 | | 129 | */ 130 | Encore.configureDevServerOptions((options) => { 131 | /** 132 | * Normalize "options.static" property to an array 133 | */ 134 | if (!options.static) { 135 | options.static = [] 136 | } else if (!Array.isArray(options.static)) { 137 | options.static = [options.static] 138 | } 139 | 140 | /** 141 | * Enable live reload and add views directory 142 | */ 143 | options.liveReload = true 144 | options.static.push({ 145 | directory: join(__dirname, './resources/views'), 146 | watch: true, 147 | }) 148 | }) 149 | 150 | /* 151 | |-------------------------------------------------------------------------- 152 | | CSS precompilers support 153 | |-------------------------------------------------------------------------- 154 | | 155 | | Uncomment one of the following lines of code to enable support for your 156 | | favorite CSS precompiler 157 | | 158 | */ 159 | // Encore.enableSassLoader() 160 | // Encore.enableLessLoader() 161 | // Encore.enableStylusLoader() 162 | 163 | /* 164 | |-------------------------------------------------------------------------- 165 | | CSS loaders 166 | |-------------------------------------------------------------------------- 167 | | 168 | | Uncomment one of the following line of code to enable support for 169 | | PostCSS or CSS. 170 | | 171 | */ 172 | // Encore.enablePostCssLoader() 173 | // Encore.configureCssLoader(() => {}) 174 | 175 | /* 176 | |-------------------------------------------------------------------------- 177 | | Enable Vue loader 178 | |-------------------------------------------------------------------------- 179 | | 180 | | Uncomment the following lines of code to enable support for vue. Also make 181 | | sure to install the required dependencies. 182 | | 183 | */ 184 | // Encore.enableVueLoader(() => {}, { 185 | // version: 3, 186 | // runtimeCompilerBuild: false, 187 | // useJsx: false 188 | // }) 189 | 190 | /* 191 | |-------------------------------------------------------------------------- 192 | | Configure logging 193 | |-------------------------------------------------------------------------- 194 | | 195 | | To keep the terminal clean from unnecessary info statements , we only 196 | | log warnings and errors. If you want all the logs, you can change 197 | | the level to "info". 198 | | 199 | */ 200 | const config = Encore.getWebpackConfig() 201 | config.infrastructureLogging = { 202 | level: 'warn', 203 | } 204 | config.stats = 'errors-warnings' 205 | 206 | /* 207 | |-------------------------------------------------------------------------- 208 | | Export config 209 | |-------------------------------------------------------------------------- 210 | | 211 | | Export config for webpack to do its job 212 | | 213 | */ 214 | module.exports = config 215 | --------------------------------------------------------------------------------