-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 470ab58
Showing
15 changed files
with
1,264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
18.1.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { auth, Client } from "../dist/index.js"; | ||
import express from "express"; | ||
|
||
const app = express(); | ||
|
||
const authClient = new auth.OAuth2User({ | ||
client_id: process.env.CLIENT_ID, | ||
client_secret: process.env.CLIENT_SECRET, | ||
callback: "http://localhost:8080/callback", | ||
scopes: ["invoices:read", "account:read", "balance:read", "invoices:create", "invoices:read", "payments:send"], | ||
token: {access_token: undefined, refresh_token: undefined, expires_at: undefined} // initialize with existing token | ||
}); | ||
|
||
const client = new Client(authClient); | ||
|
||
const STATE = "my-state"; | ||
|
||
app.get("/callback", async function (req, res) { | ||
try { | ||
const { code, state } = req.query; | ||
if (state !== STATE) return res.status(500).send("State isn't matching"); | ||
await authClient.requestAccessToken(code); | ||
console.log(authClient); | ||
const invoices = await client.accountBalance(); | ||
res.send(invoices); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
}); | ||
|
||
app.get("/login", async function (req, res) { | ||
const authUrl = authClient.generateAuthURL({ | ||
state: STATE, | ||
code_challenge_method: "S256", | ||
}); | ||
res.redirect(authUrl); | ||
}); | ||
|
||
app.get("/balance", async function (req, res) { | ||
const result = await client.accountBalance(); | ||
res.send(result); | ||
}); | ||
|
||
app.get("/summary", async function (req, res) { | ||
const result = await client.accountSummary(); | ||
res.send(result); | ||
}); | ||
|
||
app.get("/value4value", async function (req, res) { | ||
const result = await client.accountValue4Value(); | ||
res.send(result); | ||
}); | ||
|
||
app.get("/make-invoice", async function (req, res) { | ||
const result = await client.createInvoice({amount: 1000}); | ||
res.send(result); | ||
}); | ||
|
||
app.get("/bolt11/:invoice", async function(req, res) { | ||
const result = await client.sendPayment({invoice: req.params.invoice}); | ||
res.send(result); | ||
}); | ||
|
||
app.get('/keysend/:destination', async function(req, res) { | ||
const result = await client.keysend({ | ||
destination: req.params.destination, | ||
amount: 10, | ||
memo: req.query.memo | ||
}); | ||
res.send(result); | ||
}); | ||
|
||
app.get("/refresh", async function (req, res) { | ||
try { | ||
await authClient.refreshAccessToken(); | ||
res.send("Refreshed Access Token"); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
}); | ||
|
||
app.listen(8080, () => { | ||
console.log(`Go here to login: http://localhost:8080/login`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "alby-js-sdk", | ||
"version": "1.0.0", | ||
"description": "Alby OAuth2 Client", | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
"build": "tsc", | ||
"prebuild": "yarn clean", | ||
"generate": "ts-node scripts/generate.ts", | ||
"prepublishOnly": "yarn test", | ||
"build:watch": "tsc --watch", | ||
"test": "jest", | ||
"clean": "rm -rf dist" | ||
}, | ||
"dependencies": { | ||
"crypto-js": "^4.1.1", | ||
"node-fetch": "^2.6.1" | ||
}, | ||
"devDependencies": { | ||
"@types/crypto-js": "^4.1.1", | ||
"@types/node-fetch": "^2.6.1", | ||
"express": "^4.18.2" | ||
}, | ||
"jest": { | ||
"preset": "ts-jest", | ||
"testEnvironment": "node" | ||
}, | ||
"engines": { | ||
"node": ">=14" | ||
}, | ||
"author": "", | ||
"license": "MIT" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { AuthClient, AuthHeader } from "./types"; | ||
|
||
export class OAuth2Bearer implements AuthClient { | ||
private bearer_token: string; | ||
|
||
constructor(bearer_token: string) { | ||
this.bearer_token = bearer_token; | ||
} | ||
|
||
getAuthHeader(): AuthHeader { | ||
return { | ||
Authorization: `Bearer ${this.bearer_token}`, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import sha256 from 'crypto-js/sha256'; | ||
import CryptoJS from 'crypto-js'; | ||
import Base64 from 'crypto-js/enc-base64'; | ||
import { buildQueryString, basicAuthHeader } from "./utils"; | ||
import { OAuthClient, AuthHeader, GetTokenResponse, Token, GenerateAuthUrlOptions } from "./types"; | ||
import { RequestOptions, rest } from "./request"; | ||
|
||
const AUTHORIZE_URL = "https://getalby.com/oauth"; | ||
|
||
export type OAuth2Scopes = | ||
| "account:read" | ||
| "invoices:create" | ||
| "invoices:read" | ||
| "transactions:read" | ||
| "balance:read" | ||
| "payments:send"; | ||
|
||
export interface OAuth2UserOptions { | ||
client_id: string; | ||
client_secret?: string; | ||
callback: string; | ||
scopes: OAuth2Scopes[]; | ||
request_options?: Partial<RequestOptions>; | ||
token?: Token; | ||
} | ||
|
||
function processTokenResponse(token: GetTokenResponse): Token { | ||
const { expires_in, ...rest } = token; | ||
return { | ||
...rest, | ||
...(!!expires_in && { | ||
expires_at: Date.now() + expires_in * 1000, | ||
}), | ||
}; | ||
} | ||
|
||
export class OAuth2User implements OAuthClient { | ||
token?: Token; | ||
#options: OAuth2UserOptions; | ||
#code_verifier?: string; | ||
#code_challenge?: string; | ||
constructor(options: OAuth2UserOptions) { | ||
const { token, ...defaultOptions } = options; | ||
this.#options = {client_secret: '', ...defaultOptions}; | ||
this.token = token; | ||
} | ||
|
||
/** | ||
* Refresh the access token | ||
*/ | ||
async refreshAccessToken(): Promise<{ token: Token }> { | ||
const refresh_token = this.token?.refresh_token; | ||
const { client_id, client_secret, request_options } = this.#options; | ||
if (!client_id) { | ||
throw new Error("client_id is required"); | ||
} | ||
if (!refresh_token) { | ||
throw new Error("refresh_token is required"); | ||
} | ||
const data = await rest<GetTokenResponse>({ | ||
...request_options, | ||
endpoint: `/oauth/token`, | ||
params: { | ||
client_id, | ||
grant_type: "refresh_token", | ||
refresh_token, | ||
}, | ||
method: "POST", | ||
headers: { | ||
...request_options?.headers, | ||
"Content-type": "application/x-www-form-urlencoded", | ||
...{ | ||
Authorization: basicAuthHeader(client_id, client_secret), | ||
}, | ||
}, | ||
}); | ||
const token = processTokenResponse(data); | ||
this.token = token; | ||
return { token }; | ||
} | ||
|
||
/** | ||
* Check if an access token is expired | ||
*/ | ||
isAccessTokenExpired(): boolean { | ||
const refresh_token = this.token?.refresh_token; | ||
const expires_at = this.token?.expires_at; | ||
if (!expires_at) return true; | ||
return !!refresh_token && expires_at <= Date.now() + 1000; | ||
} | ||
|
||
/** | ||
* Request an access token | ||
*/ | ||
async requestAccessToken(code?: string): Promise<{ token: Token }> { | ||
const { client_id, client_secret, callback, request_options } = | ||
this.#options; | ||
const code_verifier = this.#code_verifier; | ||
if (!client_id) { | ||
throw new Error("client_id is required"); | ||
} | ||
if (!callback) { | ||
throw new Error("callback is required"); | ||
} | ||
const params = { | ||
code, | ||
grant_type: "authorization_code", | ||
code_verifier, | ||
client_id, | ||
redirect_uri: callback, | ||
}; | ||
const data = await rest<GetTokenResponse>({ | ||
...request_options, | ||
endpoint: `/oauth/token`, | ||
params, | ||
method: "POST", | ||
headers: { | ||
...request_options?.headers, | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
...{ | ||
Authorization: basicAuthHeader(client_id, client_secret), | ||
}, | ||
}, | ||
}); | ||
const token = processTokenResponse(data); | ||
this.token = token; | ||
return { token }; | ||
} | ||
|
||
generateAuthURL(options: GenerateAuthUrlOptions): string { | ||
const { client_id, callback, scopes } = this.#options; | ||
if (!callback) throw new Error("callback required"); | ||
if (!scopes) throw new Error("scopes required"); | ||
if (options.code_challenge_method === "S256") { | ||
const code_verifier = CryptoJS.lib.WordArray.random(64); | ||
this.#code_verifier = code_verifier.toString(); | ||
this.#code_challenge = sha256(this.#code_verifier).toString(Base64).replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '') | ||
} else { | ||
this.#code_challenge = options.code_challenge; | ||
this.#code_verifier = options.code_challenge; | ||
} | ||
const code_challenge = this.#code_challenge; | ||
const url = new URL(AUTHORIZE_URL); | ||
url.search = buildQueryString({ | ||
...options, | ||
client_id, | ||
scope: scopes.join(" "), | ||
response_type: "code", | ||
redirect_uri: callback, | ||
code_challenge_method: options.code_challenge_method || "plain", | ||
code_challenge, | ||
}); | ||
return url.toString(); | ||
} | ||
|
||
async getAuthHeader(): Promise<AuthHeader> { | ||
if (!this.token?.access_token) throw new Error("access_token is required"); | ||
if (this.isAccessTokenExpired()) await this.refreshAccessToken(); | ||
return { | ||
Authorization: `Bearer ${this.token.access_token}`, | ||
}; | ||
} | ||
} |
Oops, something went wrong.