diff --git a/README.md b/README.md index e9b460b..a44045c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # SuperJWT +![npm version](https://img.shields.io/badge/npm-0.0.7-brightgreen) + Super-JWT is a Node.js package that helps authenticate users based on Superfluid streams using JSON Web Tokens (JWT). #### Installation @@ -26,13 +28,13 @@ To authenticate a user with a Superfluid stream, use the `authenticateWithStream An object that represents the required parameters for authenticating a Superfluid stream. It has the following properties: -`chain`: A Chain value that represents the Ethereum chain on which the Superfluid stream is created. +`chain`: A Chain value that represents the chain on which the Superfluid stream is created. See [supported chains](#supported-chains) for more information. -`sender`: A string that represents the Ethereum address of the sender of the stream. +`sender`: A string that represents the ethereum address of the sender of the stream. -`receiver`: A string that represents the Ethereum address of the receiver of the stream. +`receiver`: A string that represents the ethereum address of the receiver of the stream. -`token`: A string that represents the address of the ERC20 token used in the Superfluid stream. It can be the address of the underlying SuperToken. +`token`: A string that represents the ethereum address of the SuperToken being used. In addition to the required parameters mentioned earlier, you can also pass any of the `where` parameters of the Superfluid subgraph `streams` query. This allows you to filter streams based on other properties such as `currentFlowRate_gt`, which is the flow rate in the stream. For more information on the available query parameters, you can refer to the [Superfluid subgraph documentation](https://thegraph.com/hosted-service/subgraph/superfluid-finance/protocol-v1-goerli). @@ -91,6 +93,43 @@ console.log(decoded); // } ``` +#### Supported Chains + +Super-JWT supports the following chains: + +```ts +type Chain = + | "goerli" + | "mumbai" + | "matic" + | "mainnet" + | "opgoerli" + | "arbgoerli" + | "fuji" + | "xdai" + | "optimism" + | "avalanche" + | "bsc" + | "celo" + | "base"; +``` + +### Publishing + +To publish a new version of the package to npm, run the following command: + +```shell +npm run publish +``` + +### Change Log + +#### 0.0.7 + +- Added more chains to the supported chains list. see [supported chains](#supported-chains) for more information. +- Throw an error if the chain is not supported instead of using default chain. +- Better error handling. + ### Safety This is experimental software and subject to change over time. diff --git a/package.json b/package.json index f80d6c9..844c4f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "super-jwt", - "version": "0.0.6", + "version": "0.0.7", "description": "Super-JWT helps to authenticate your users based on Superfluid streams using JSON Web Tokens (JWT)", "author": "Salman Dabbakuti", "license": "MIT", @@ -16,22 +16,21 @@ "superfluid-auth" ], "dependencies": { - "graphql": "^16.6.0", - "graphql-request": "^5.2.0", - "jsonwebtoken": "^9.0.0" + "graphql": "^16.8.1", + "graphql-request": "^6.1.0", + "jsonwebtoken": "^9.0.2" }, "devDependencies": { - "@types/chai": "^4.3.4", - "@types/chai-as-promised": "^7.1.5", - "@types/jsonwebtoken": "^9.0.1", - "@types/mocha": "^10.0.1", - "chai": "^4.3.7", + "@types/chai": "^4.3.6", + "@types/chai-as-promised": "^7.1.6", + "@types/jsonwebtoken": "^9.0.3", + "@types/mocha": "^10.0.2", + "chai": "^4.3.10", "chai-as-promised": "^7.1.1", "mocha": "^10.2.0", - "super-jwt": "^0.0.2", "ts-node": "^10.9.1", - "tslib": "^2.5.0", - "typescript": "^5.0.3" + "tslib": "^2.6.2", + "typescript": "^5.2.2" }, "repository": { "type": "git", diff --git a/src/index.ts b/src/index.ts index f879d53..16842c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,20 @@ import { request, gql } from "graphql-request"; import jwt, { JwtPayload, SignOptions, Secret } from "jsonwebtoken"; -export type Chain = "goerli" | "mumbai" | "matic"; +export type Chain = + | "goerli" + | "mumbai" + | "matic" + | "mainnet" + | "opgoerli" + | "arbgoerli" + | "fuji" + | "xdai" + | "optimism" + | "avalanche" + | "bsc" + | "celo" + | "base"; interface Stream { id: string; @@ -12,57 +25,97 @@ interface StreamQueryResult { } export interface StreamPayload { - chain: Chain | string; + chain: Chain; sender: string; receiver: string; token: string; [key: string]: any; } + export interface AuthenticationResult { token: string; stream: StreamPayload | JwtPayload; } -const defaultSubgraphUrls: Record = { +const defaultSubgraphUrls: Record = { goerli: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-goerli", mumbai: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-mumbai", matic: - "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-matic" + "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-matic", + mainnet: + "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-eth-mainnet", + opgoerli: + "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-optimism-goerli", + arbgoerli: + "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-arbitrum-goerli", + fuji: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-avalanche-fuji", + xdai: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-xdai", + optimism: + "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-optimism-mainnet", + avalanche: + "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-avalanche-c", + bsc: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-bsc-mainnet", + celo: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-celo-mainnet", + base: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-base" }; +const STREAMS_QUERY = gql` + query GetStreams($first: Int, $where: Stream_filter) { + streams(first: $first, where: $where) { + id + } + } +`; + // Retrieve streams using the Superfluid subgraph -const getStreams = async ({ +async function getStreams({ chain, ...streamWhereParams -}: StreamPayload): Promise => { - const STREAMS_QUERY = gql` - query GetStreams($first: Int, $where: Stream_filter) { - streams(first: $first, where: $where) { - id - } - } - `; - const subgraphUrl = - defaultSubgraphUrls[chain] || defaultSubgraphUrls["goerli"]; - const { streams }: StreamQueryResult = await request( - subgraphUrl, - STREAMS_QUERY, - { - first: 1, - where: { - ...streamWhereParams +}: StreamPayload): Promise { + const subgraphUrl = defaultSubgraphUrls[chain]; + if (!subgraphUrl) + throw new Error(`super-jwt: Chain ${chain} is not supported`); + try { + const { streams }: StreamQueryResult = await request( + subgraphUrl, + STREAMS_QUERY, + { + first: 1, + where: { + ...streamWhereParams + } } - } - ); - return streams; -}; + ); + return streams; + } catch (err) { + console.warn(`super-jwt: Failed to retrieve streams: ${err}`); + return []; + } +} + +export async function authenticateWithStream( + streamPayload: StreamPayload, + secret: Secret, + jwtOptions: SignOptions +): Promise { + const requiredParams = ["chain", "sender", "receiver", "token"]; + + if (!requiredParams.every((param) => param in streamPayload)) { + throw new Error( + "super-jwt: Missing required stream payload params: chain, sender, receiver, token" + ); + } -export async function authenticateWithStream(streamPayload: StreamPayload, secret: Secret, jwtOptions: SignOptions): Promise { - if (!["chain", "sender", "receiver", "token"].every((param) => streamPayload.hasOwnProperty(param))) throw new Error("super-jwt: Missing required stream payload params: chain, sender, receiver, token"); const streams = await getStreams(streamPayload); - if (!streams || streams.length === 0) throw new Error("super-jwt: No stream found to authenticate between sender and receiver"); + + if (!streams || streams.length === 0) { + throw new Error( + "super-jwt: No stream found to authenticate between sender and receiver" + ); + } + const jwtToken = jwt.sign(streamPayload, secret, jwtOptions); return { token: jwtToken, stream: streamPayload }; } @@ -71,7 +124,7 @@ export function verifyToken(jwtToken: string, secret: Secret): JwtPayload { try { const decoded = jwt.verify(jwtToken, secret) as JwtPayload; return decoded; - } catch (err: unknown) { + } catch (err) { throw new Error(`super-jwt: failed to verify token: ${err}`); } -} \ No newline at end of file +} diff --git a/test/super-jwt.test.ts b/test/super-jwt.test.ts index d9fdbd7..d2d9f41 100644 --- a/test/super-jwt.test.ts +++ b/test/super-jwt.test.ts @@ -1,57 +1,101 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -// import jwt from 'jsonwebtoken'; -import { authenticateWithStream, verifyToken } from "../src/index.ts"; +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { authenticateWithStream, verifyToken, Chain } from "../src/index"; chai.use(chaiAsPromised); const { expect } = chai; const streamPayload = { - chain: 'goerli', - sender: '0xc7203561ef179333005a9b81215092413ab86ae9', - receiver: '0x7348943c8d263ea253c0541656c36b88becd77b9', - token: '0xf2d68898557ccb2cf4c10c3ef2b034b2a69dad00', + chain: "goerli" as Chain, + sender: "0xc7203561ef179333005a9b81215092413ab86ae9", + receiver: "0x7348943c8d263ea253c0541656c36b88becd77b9", + token: "0xf2d68898557ccb2cf4c10c3ef2b034b2a69dad00", currentFlowRate_gt: 1 }; -const testSecret = 'supersecret'; -const testJwtOptions = { expiresIn: '1h' }; +const testSecret = "supersecret"; +const testJwtOptions = { expiresIn: "1h" }; -describe('authenticateWithStream', () => { - it('should throw an error if required stream payload fields are missing', async () => { - const invalidStreamPayload = { chain: 'goerli', sender: '0xc7203561ef179333005a9b81215092413ab86ae9' } as typeof streamPayload; - await expect(authenticateWithStream(invalidStreamPayload, testSecret, testJwtOptions)).to.be.rejectedWith(Error, 'super-jwt: Missing required stream payload'); +describe("authenticateWithStream", () => { + it("should throw an error if chain is not supported", async () => { + const unsupportedChainStreamPayload = { + ...streamPayload, + chain: "near" as Chain + } as typeof streamPayload; + await expect( + authenticateWithStream( + unsupportedChainStreamPayload, + testSecret, + testJwtOptions + ) + ).to.be.rejectedWith(Error, "super-jwt: Chain near is not supported"); }); - it('should throw an error if no streams are found', async () => { - const emptyStreamPayload = { ...streamPayload, sender: '0x00000000000000000a5000000000000000000009' }; - await expect(authenticateWithStream(emptyStreamPayload, testSecret, testJwtOptions)).to.be.rejectedWith(Error, 'super-jwt: No stream found to authenticate'); + it("should throw an error if required stream payload fields are missing", async () => { + const invalidStreamPayload = { + chain: "goerli", + sender: "0xc7203561ef179333005a9b81215092413ab86ae9" + } as typeof streamPayload; + await expect( + authenticateWithStream(invalidStreamPayload, testSecret, testJwtOptions) + ).to.be.rejectedWith(Error, "super-jwt: Missing required stream payload"); }); - it('should return a token and stream payload', async () => { - const result = await authenticateWithStream(streamPayload, testSecret, testJwtOptions); - expect(result).to.have.property('token'); - expect(result).to.have.property('stream').deep.equal(streamPayload); + it("should throw an error if no streams are found", async () => { + const emptyStreamPayload = { + ...streamPayload, + sender: "0x00000000000000000a5000000000000000000009" + }; + await expect( + authenticateWithStream(emptyStreamPayload, testSecret, testJwtOptions) + ).to.be.rejectedWith(Error, "super-jwt: No stream found to authenticate"); + }); + + it("should return a token and stream payload", async () => { + const result = await authenticateWithStream( + streamPayload, + testSecret, + testJwtOptions + ); + expect(result).to.have.property("token"); + expect(result).to.have.property("stream").deep.equal(streamPayload); }); }); -describe('verifyToken', () => { - it('should throw an error if token is invalid', () => { - const invalidToken = 'invalidtoken'; - expect(() => verifyToken(invalidToken, testSecret)).to.throw(Error, 'super-jwt: failed to verify token'); +describe("verifyToken", () => { + it("should throw an error if token is invalid", () => { + const invalidToken = "invalidtoken"; + expect(() => verifyToken(invalidToken, testSecret)).to.throw( + Error, + "super-jwt: failed to verify token" + ); }); - it('should throw an error if wrong secret used', () => { - const invalidToken = 'invalidtoken'; - expect(() => verifyToken(invalidToken, "somewrongsecret")).to.throw(Error, 'super-jwt: failed to verify token'); + it("should throw an error if wrong secret used", () => { + const invalidToken = "invalidtoken"; + expect(() => verifyToken(invalidToken, "somewrongsecret")).to.throw( + Error, + "super-jwt: failed to verify token" + ); }); - it('should throw an error if token is expired', () => { - const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOiIxNTE2MjM5MDk5In0.YQDJ1bYgNrI7oLqPSTPv95-CSryj9Fv0TadONH5aukY'; - expect(() => verifyToken(expiredToken, testSecret)).to.throw(Error, 'super-jwt: failed to verify token'); + it("should throw an error if token is expired", () => { + const expiredToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOiIxNTE2MjM5MDk5In0.YQDJ1bYgNrI7oLqPSTPv95-CSryj9Fv0TadONH5aukY"; + expect(() => verifyToken(expiredToken, testSecret)).to.throw( + Error, + "super-jwt: failed to verify token" + ); }); - it('should return the decoded token payload', async () => { - const { token: jwtToken } = await authenticateWithStream(streamPayload, testSecret, testJwtOptions); - const { chain, sender, receiver, token, currentFlowRate_gt } = verifyToken(jwtToken, testSecret); + it("should return the decoded token payload", async () => { + const { token: jwtToken } = await authenticateWithStream( + streamPayload, + testSecret, + testJwtOptions + ); + const { chain, sender, receiver, token, currentFlowRate_gt } = verifyToken( + jwtToken, + testSecret + ); const payload = { chain, sender,