diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..1b6457c
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+}
diff --git a/README.md b/README.md
index d1d8e08..db0be8d 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,31 @@ This JavaScript SDK for the Alby OAuth2 Wallet API and the Nostr Wallet Connect
npm install @getalby/sdk
```
+or
+
+```
+yarn add @getalby/sdk
+```
+
+or for use without any build tools:
+
+```html
+
+```
+
**This library relies on a global fetch() function which will work in browsers and node v18.x or newer.** (In older versions you have to use a polyfill.)
## Content
@@ -127,6 +152,7 @@ catch (e) {
```
#### React Native (Expo)
+
Look at our [NWC React Native Expo Demo app](https://github.com/getAlby/nwc-react-native-expo) for how to use NWC in a React Native expo project.
#### For Node.js
@@ -249,7 +275,7 @@ const authClient = new auth.OAuth2User({
}, // initialize with existing token
});
-const authUrl = authClient.generateAuthURL({
+const authUrl = await authClient.generateAuthURL({
code_challenge_method: "S256",
// authorizeUrl: "https://getalby.com/oauth" endpoint for authorization (replace with the appropriate URL based on the environment)
});
diff --git a/examples/boostagram.js b/examples/boostagram.js
index 70fd57d..1ce3712 100644
--- a/examples/boostagram.js
+++ b/examples/boostagram.js
@@ -25,7 +25,7 @@ const authClient = new auth.OAuth2User({
});
console.log(`Open the following URL and authenticate the app:`);
-console.log(authClient.generateAuthURL());
+console.log(await authClient.generateAuthURL());
console.log("----\n");
const code = await rl.question("Code: (localhost:8080?code=[THIS CODE]: ");
diff --git a/examples/decode-invoice.js b/examples/decode-invoice.js
index e2771aa..2109d51 100644
--- a/examples/decode-invoice.js
+++ b/examples/decode-invoice.js
@@ -20,7 +20,7 @@ const authClient = new auth.OAuth2User({
});
console.log(`Open the following URL and authenticate the app:`);
-console.log(authClient.generateAuthURL());
+console.log(await authClient.generateAuthURL());
console.log("----\n");
const code = await rl.question("Code: (localhost:8080?code=[THIS CODE]: ");
diff --git a/examples/invoices.js b/examples/invoices.js
index 03ef621..0e4c25c 100644
--- a/examples/invoices.js
+++ b/examples/invoices.js
@@ -18,7 +18,7 @@ const authClient = new auth.OAuth2User({
});
console.log(`Open the following URL and authenticate the app:`);
-console.log(authClient.generateAuthURL());
+console.log(await authClient.generateAuthURL());
console.log("----\n");
const code = await rl.question("Code: (localhost:8080?code=[THIS CODE]: ");
diff --git a/examples/keysends.js b/examples/keysends.js
index 4db7783..fd76078 100644
--- a/examples/keysends.js
+++ b/examples/keysends.js
@@ -25,7 +25,7 @@ const authClient = new auth.OAuth2User({
});
console.log(`Open the following URL and authenticate the app:`);
-console.log(authClient.generateAuthURL());
+console.log(await authClient.generateAuthURL());
console.log("----\n");
const code = await rl.question("Code: (localhost:8080?code=[THIS CODE]: ");
diff --git a/examples/nwc/get-balance.js b/examples/nwc/get-balance.js
index 48e45d1..1e77cd3 100644
--- a/examples/nwc/get-balance.js
+++ b/examples/nwc/get-balance.js
@@ -9,9 +9,9 @@ import { webln as providers } from "../../dist/index.module.js";
const rl = readline.createInterface({ input, output });
-const nwcUrl = await rl.question(
- "Nostr Wallet Connect URL (nostr+walletconnect://...): ",
-);
+const nwcUrl =
+ process.env.NWC_URL ||
+ (await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): "));
rl.close();
const webln = new providers.NostrWebLNProvider({
diff --git a/examples/oauth2-public-callback_pkce_s256.mjs b/examples/oauth2-public-callback_pkce_s256.mjs
index 7311c8d..f91c941 100644
--- a/examples/oauth2-public-callback_pkce_s256.mjs
+++ b/examples/oauth2-public-callback_pkce_s256.mjs
@@ -1,14 +1,29 @@
import { auth, Client } from "../dist/index.module.js";
import express from "express";
+if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) {
+ throw new Error("Please set CLIENT_ID and CLIENT_SECRET");
+}
+
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
+ 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);
@@ -29,7 +44,7 @@ app.get("/callback", async function (req, res) {
});
app.get("/login", async function (req, res) {
- const authUrl = authClient.generateAuthURL({
+ const authUrl = await authClient.generateAuthURL({
state: STATE,
code_challenge_method: "S256",
});
@@ -52,20 +67,20 @@ app.get("/value4value", async function (req, res) {
});
app.get("/make-invoice", async function (req, res) {
- const result = await client.createInvoice({amount: 1000});
+ 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});
+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) {
+app.get("/keysend/:destination", async function (req, res) {
const result = await client.keysend({
destination: req.params.destination,
amount: 10,
- memo: req.query.memo
+ memo: req.query.memo,
});
res.send(result);
});
diff --git a/examples/send-to-ln-address.js b/examples/send-to-ln-address.js
index 568f5e3..307b46e 100644
--- a/examples/send-to-ln-address.js
+++ b/examples/send-to-ln-address.js
@@ -19,7 +19,7 @@ const authClient = new auth.OAuth2User({
});
console.log(`Open the following URL and authenticate the app:`);
-console.log(authClient.generateAuthURL());
+console.log(await authClient.generateAuthURL());
console.log("----\n");
const code = await rl.question("Code: (localhost:8080?code=[THIS CODE]: ");
diff --git a/examples/webhooks.js b/examples/webhooks.js
index e8f50d2..a2cf29a 100644
--- a/examples/webhooks.js
+++ b/examples/webhooks.js
@@ -25,7 +25,7 @@ const authClient = new auth.OAuth2User({
});
console.log(`Open the following URL and authenticate the app:`);
-console.log(authClient.generateAuthURL());
+console.log(await authClient.generateAuthURL());
console.log("----\n");
const code = await rl.question("Code: (localhost:8080?code=[THIS CODE]: ");
diff --git a/package.json b/package.json
index 43955f1..f1aabda 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@getalby/sdk",
- "version": "2.7.0",
+ "version": "3.0.0",
"description": "The SDK to integrate with Nostr Wallet Connect and the Alby API",
"repository": "https://github.com/getAlby/js-sdk.git",
"bugs": "https://github.com/getAlby/js-sdk/issues",
@@ -33,20 +33,17 @@
"prepack": "yarn run build",
"test": "jest",
"clean": "rm -rf dist",
- "build:browser": "cp src/window.js dist && browserify dist/window.js > dist/index.browser.js",
- "build": "microbundle && yarn build:browser",
+ "build": "microbundle --no-sourcemap",
"dev": "microbundle watch",
"prepare": "husky install"
},
"dependencies": {
- "crypto-js": "^4.1.1",
"nostr-tools": "^1.17.0",
"events": "^3.3.0"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
- "@types/crypto-js": "^4.1.1",
"@types/jest": "^29.5.5",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.3.0",
diff --git a/src/OAuth2User.ts b/src/OAuth2User.ts
index be52dc4..6196a25 100644
--- a/src/OAuth2User.ts
+++ b/src/OAuth2User.ts
@@ -1,5 +1,4 @@
-import CryptoJS from "crypto-js";
-import { buildQueryString, basicAuthHeader } from "./utils";
+import { buildQueryString, basicAuthHeader, toHexString } from "./utils";
import {
OAuthClient,
AuthHeader,
@@ -75,6 +74,7 @@ export class OAuth2User implements OAuthClient {
if (this._refreshAccessTokenPromise) {
return this._refreshAccessTokenPromise;
}
+ // eslint-disable-next-line no-async-promise-executor
this._refreshAccessTokenPromise = new Promise(async (resolve, reject) => {
try {
const refresh_token = this.token?.refresh_token;
@@ -109,7 +109,7 @@ export class OAuth2User implements OAuthClient {
resolve({ token });
this._tokenEvents.emit("tokenRefreshed", this.token);
} catch (error) {
- console.log(error);
+ console.error(error);
reject(error);
this._tokenEvents.emit("tokenRefreshFailed", error);
} finally {
@@ -140,7 +140,9 @@ export class OAuth2User implements OAuthClient {
throw new Error("client_id is required");
}
if (!client_secret && !code_verifier) {
- throw new Error("either client_secret is required, or code should be generated using a challenge");
+ throw new Error(
+ "either client_secret is required, or code should be generated using a challenge",
+ );
}
if (!callback) {
throw new Error("callback is required");
@@ -171,23 +173,16 @@ export class OAuth2User implements OAuthClient {
return { token };
}
- generateAuthURL(options?: GenerateAuthUrlOptions): string {
+ async generateAuthURL(options?: GenerateAuthUrlOptions): Promise {
if (!options) {
options = {};
}
- console.log(options);
const { client_id, callback, scopes } = this.options;
if (!callback) throw new Error("callback required");
if (!scopes) throw new Error("scopes required");
let code_challenge_method;
if (options.code_challenge_method === "S256") {
- const code_verifier = CryptoJS.lib.WordArray.random(64);
- this.code_verifier = code_verifier.toString();
- this.code_challenge = CryptoJS.SHA256(this.code_verifier)
- .toString(CryptoJS.enc.Base64)
- .replace(/\+/g, "-")
- .replace(/\//g, "_")
- .replace(/\=+$/, "");
+ await this._generateS256Challenge();
code_challenge_method = "S256";
} else if (
options.code_challenge_method === "plain" &&
@@ -220,4 +215,24 @@ export class OAuth2User implements OAuthClient {
Authorization: `Bearer ${this.token.access_token}`,
};
}
+
+ private async _generateS256Challenge() {
+ const codeVerifierBytes = crypto.getRandomValues(new Uint8Array(64));
+ this.code_verifier = toHexString(codeVerifierBytes);
+
+ // from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
+ const hashBuffer = await crypto.subtle.digest(
+ "SHA-256",
+ new TextEncoder().encode(this.code_verifier),
+ );
+ const hashArray = new Uint8Array(hashBuffer);
+
+ // from https://stackoverflow.com/a/45313868
+ // TODO: consider using Buffer.from(hashBuffer).toString("base64") in NodeJS
+ this.code_challenge = btoa(String.fromCharCode(...hashArray))
+ // from https://gist.github.com/jhurliman/1250118?permalink_comment_id=3194799
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/, "");
+ }
}
diff --git a/src/types.ts b/src/types.ts
index 3534627..160d0ec 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -47,7 +47,7 @@ export type GenerateAuthUrlOptions = { authorizeUrl?: string } & (
export abstract class OAuthClient implements AuthClient {
abstract token?: Token;
- abstract generateAuthURL(options: GenerateAuthUrlOptions): string;
+ abstract generateAuthURL(options: GenerateAuthUrlOptions): Promise;
abstract requestAccessToken(code?: string): Promise<{ token: Token }>;
abstract getAuthHeader(
url?: string,
diff --git a/src/utils.ts b/src/utils.ts
index 314fc19..eb1245a 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -12,3 +12,7 @@ export function basicAuthHeader(
) {
return `Basic ${btoa(`${client_id}:${client_secret}`)}`;
}
+
+// from https://stackoverflow.com/a/50868276
+export const toHexString = (bytes: Uint8Array) =>
+ bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
diff --git a/src/webln/OauthWeblnProvider.ts b/src/webln/OauthWeblnProvider.ts
index 9230007..74318af 100644
--- a/src/webln/OauthWeblnProvider.ts
+++ b/src/webln/OauthWeblnProvider.ts
@@ -13,7 +13,7 @@ export class OauthWeblnProvider {
client: Client;
auth: OAuthClient;
oauth: boolean;
- subscribers: Record void>;
+ subscribers: Record void>;
isExecuting: boolean;
constructor(options: { auth: OAuthClient }) {
@@ -28,7 +28,7 @@ export class OauthWeblnProvider {
this.subscribers[name] = callback;
}
- notify(name: string, payload?: any) {
+ notify(name: string, payload?: unknown) {
const callback = this.subscribers[name];
if (callback) {
callback(payload);
@@ -45,7 +45,7 @@ export class OauthWeblnProvider {
if (isBrowser()) {
try {
this.isExecuting = true;
- const result = await this.openAuthorization();
+ await this.openAuthorization();
} finally {
this.isExecuting = false;
}
@@ -123,12 +123,14 @@ export class OauthWeblnProvider {
}
}
- openAuthorization() {
+ async openAuthorization() {
const height = 700;
const width = 600;
const top = window.outerHeight / 2 + window.screenY - height / 2;
const left = window.outerWidth / 2 + window.screenX - width / 2;
- const url = this.auth.generateAuthURL({ code_challenge_method: "S256" });
+ const url = await this.auth.generateAuthURL({
+ code_challenge_method: "S256",
+ });
return new Promise((resolve, reject) => {
const popup = window.open(
diff --git a/src/window.js b/src/window.js
deleted file mode 100644
index dd73d5a..0000000
--- a/src/window.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// assign alby-sdk exports to global window object (for index.browser.js)
-// @ts-ignore
-window["albySDK"] = require("./index.cjs");
diff --git a/tsconfig.json b/tsconfig.json
index 5e063b5..207ce4a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,12 +6,13 @@
"skipLibCheck": true,
"checkJs": true,
"allowJs": true,
- "declarationMap": true,
+ "declarationMap": false,
"declaration": true,
"allowSyntheticDefaultImports": true,
"target": "es2020",
+ "module": "ESNext",
"rootDir": "./src",
- "sourceMap": true,
+ "sourceMap": false,
"moduleResolution": "node"
},
"include": ["src/**/*"],
diff --git a/yarn.lock b/yarn.lock
index 3755a76..ddbbb40 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2021,11 +2021,6 @@
dependencies:
"@babel/types" "^7.20.7"
-"@types/crypto-js@^4.1.1":
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d"
- integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==
-
"@types/estree@*":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
@@ -3256,11 +3251,6 @@ crypto-browserify@^3.0.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
-crypto-js@^4.1.1:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
- integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
-
css-declaration-sorter@^6.3.0:
version "6.3.1"
resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec"