@bananalink-test/client 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,319 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let jose = require("jose");
30
+ let qrcode = require("qrcode");
31
+ qrcode = __toESM(qrcode);
32
+
33
+ //#region src/errors/BananalinkError.ts
34
+ var BananalinkError = class extends Error {
35
+ constructor(message, options) {
36
+ super(message, options);
37
+ this.name = "BananalinkError";
38
+ }
39
+ };
40
+
41
+ //#endregion
42
+ //#region src/errors/DappApiError.ts
43
+ var DappApiError = class extends BananalinkError {
44
+ constructor(message, statusCode) {
45
+ super(message);
46
+ this.statusCode = statusCode;
47
+ this.name = "DappApiError";
48
+ }
49
+ };
50
+
51
+ //#endregion
52
+ //#region src/api/createDappInstance.ts
53
+ function isCreateDappInstanceResponse(body) {
54
+ return typeof body === "object" && body !== null && "dappInstanceId" in body && typeof body.dappInstanceId === "string" && "nonce" in body && typeof body.nonce === "string";
55
+ }
56
+ async function createDappInstance(apiUrl, params) {
57
+ const response = await fetch(`${apiUrl}/dapps`, {
58
+ headers: { "Content-Type": "application/json" },
59
+ method: "POST",
60
+ body: JSON.stringify(params)
61
+ });
62
+ if (response.ok) {
63
+ const body = await response.json().catch(() => {
64
+ throw new DappApiError("Invalid JSON response from bananalink API", response.status);
65
+ });
66
+ if (!isCreateDappInstanceResponse(body)) throw new DappApiError("Unexpected response shape from bananalink API", response.status);
67
+ return body;
68
+ }
69
+ throw new DappApiError("Failed to create dapp instance", response.status);
70
+ }
71
+
72
+ //#endregion
73
+ //#region src/errors/InvalidConfigError.ts
74
+ var InvalidConfigError = class extends BananalinkError {
75
+ constructor(message) {
76
+ super(message);
77
+ this.name = "InvalidConfigError";
78
+ }
79
+ };
80
+
81
+ //#endregion
82
+ //#region src/jwt/createBananalinkJwks.ts
83
+ function createBananalinkJwks(apiUrl) {
84
+ return (0, jose.createRemoteJWKSet)(new URL(`${apiUrl}/.well-known/jwks.json`));
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/jwt/verifyJwt.ts
89
+ async function verifyJwt(jwt, jwks) {
90
+ try {
91
+ const { payload } = await (0, jose.jwtVerify)(jwt, jwks, {
92
+ algorithms: ["ES256"],
93
+ issuer: "bananalink",
94
+ requiredClaims: ["sub", "aud"]
95
+ });
96
+ return {
97
+ address: payload.sub,
98
+ dappId: Array.isArray(payload.aud) ? payload.aud[0] : payload.aud
99
+ };
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ //#endregion
106
+ //#region src/utils/isValidUrl.ts
107
+ function isValidUrl(url, protocols) {
108
+ try {
109
+ const parsed = new URL(url);
110
+ return protocols.includes(parsed.protocol.replace(":", ""));
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ //#endregion
117
+ //#region src/errors/ConnectionRejectedError.ts
118
+ var ConnectionRejectedError = class extends BananalinkError {
119
+ constructor() {
120
+ super("Dapp connection attempt was rejected by the wallet");
121
+ this.name = "ConnectionRejectedError";
122
+ }
123
+ };
124
+
125
+ //#endregion
126
+ //#region src/errors/ConnectionTimeoutError.ts
127
+ var ConnectionTimeoutError = class extends BananalinkError {
128
+ constructor() {
129
+ super("Dapp connection attempt timed out");
130
+ this.name = "ConnectionTimeoutError";
131
+ }
132
+ };
133
+
134
+ //#endregion
135
+ //#region src/errors/InvalidConnectionStateError.ts
136
+ var InvalidConnectionStateError = class extends BananalinkError {
137
+ constructor(message) {
138
+ super(message);
139
+ this.name = "InvalidConnectionStateError";
140
+ }
141
+ };
142
+
143
+ //#endregion
144
+ //#region src/core/BananalinkSession.ts
145
+ var BananalinkSession = class BananalinkSession {
146
+ static PING_INTERVAL_MILLIS = 30 * 1e3;
147
+ static PONG_TIMEOUT_MILLIS = 5 * 1e3;
148
+ pingTimer = null;
149
+ pongTimer = null;
150
+ constructor(ws) {
151
+ this.ws = ws;
152
+ this.ws.addEventListener("message", (event) => {
153
+ const { type } = JSON.parse(event.data);
154
+ if (type === "pong" && this.pongTimer) {
155
+ clearTimeout(this.pongTimer);
156
+ this.pongTimer = null;
157
+ }
158
+ });
159
+ this.startPing();
160
+ }
161
+ close() {
162
+ this.stopTimers();
163
+ this.ws.close();
164
+ }
165
+ startPing() {
166
+ this.pingTimer = setInterval(() => this.ping(), BananalinkSession.PING_INTERVAL_MILLIS);
167
+ }
168
+ ping() {
169
+ if (this.ws.readyState !== WebSocket.OPEN) {
170
+ this.close();
171
+ return;
172
+ }
173
+ this.ws.send(JSON.stringify({ type: "ping" }));
174
+ this.pongTimer = setTimeout(() => this.close(), BananalinkSession.PONG_TIMEOUT_MILLIS);
175
+ }
176
+ stopTimers() {
177
+ if (this.pingTimer !== null) clearInterval(this.pingTimer);
178
+ if (this.pongTimer !== null) clearTimeout(this.pongTimer);
179
+ this.pingTimer = null;
180
+ this.pongTimer = null;
181
+ }
182
+ };
183
+
184
+ //#endregion
185
+ //#region src/core/BananalinkConnection.ts
186
+ var BananalinkConnection = class BananalinkConnection {
187
+ static AUTH_TIMEOUT_MILLIS = 60 * 1e3;
188
+ wsUrl;
189
+ jwt;
190
+ dappId;
191
+ dappInstanceId;
192
+ nonce;
193
+ constructor(opts) {
194
+ this.wsUrl = opts.wsUrl;
195
+ this.jwt = opts.jwt;
196
+ this.dappId = opts.dappId;
197
+ this.dappInstanceId = opts.dappInstanceId;
198
+ this.nonce = opts.nonce;
199
+ }
200
+ async getSession() {
201
+ if (this.jwt) throw new BananalinkError("Recovering a lost dapp connection is not yet implemented");
202
+ const dappInstanceId = this.dappInstanceId;
203
+ if (!dappInstanceId) throw new InvalidConnectionStateError("Cannot get session without authorizing the dapp instance or providing a valid JWT");
204
+ return new Promise((resolve, reject) => {
205
+ const ws = new WebSocket(`${this.wsUrl}?dappInstanceId=${encodeURIComponent(dappInstanceId)}`);
206
+ const authTimeout = setTimeout(() => {
207
+ ws.close();
208
+ reject(new ConnectionTimeoutError());
209
+ }, BananalinkConnection.AUTH_TIMEOUT_MILLIS);
210
+ const onMessage = (event) => {
211
+ const msg = JSON.parse(event.data);
212
+ clearTimeout(authTimeout);
213
+ ws.removeEventListener("message", onMessage);
214
+ if (msg.type === "authorized") {
215
+ BananalinkConnection.bind(ws, msg.accessToken);
216
+ resolve(new BananalinkSession(ws));
217
+ } else if (msg.type === "rejected") {
218
+ ws.close();
219
+ reject(new ConnectionRejectedError());
220
+ }
221
+ };
222
+ ws.addEventListener("message", onMessage);
223
+ ws.addEventListener("error", () => {});
224
+ ws.addEventListener("close", () => {});
225
+ });
226
+ }
227
+ get connectionUrl() {
228
+ if (!this.dappInstanceId || !this.nonce) throw new InvalidConnectionStateError("Cannot get connection URL without a dapp instance");
229
+ const dappInstanceId = this.dappInstanceId;
230
+ const nonce = this.nonce;
231
+ return `bananalink://?dappId=${encodeURIComponent(this.dappId)}&dappInstanceId=${encodeURIComponent(dappInstanceId)}&nonce=${encodeURIComponent(nonce)}`;
232
+ }
233
+ get authorized() {
234
+ return this.jwt !== void 0;
235
+ }
236
+ static bind(ws, jwt) {
237
+ ws.send(JSON.stringify({
238
+ type: "bind",
239
+ jwt
240
+ }));
241
+ }
242
+ };
243
+
244
+ //#endregion
245
+ //#region src/core/BananalinkClient.ts
246
+ var BananalinkClient = class BananalinkClient {
247
+ apiUrl;
248
+ wsUrl;
249
+ jwks;
250
+ dapp;
251
+ static DEFAULT_BANANALINK_API_URL = "https://tfr9p2mn4i.execute-api.us-east-2.amazonaws.com";
252
+ static DEFAULT_BANANALINK_WS_URL = "wss://yb47qomkt3.execute-api.us-east-2.amazonaws.com/v1";
253
+ constructor(config) {
254
+ this.apiUrl = config.apiUrl ?? BananalinkClient.DEFAULT_BANANALINK_API_URL;
255
+ this.wsUrl = config.wsUrl ?? BananalinkClient.DEFAULT_BANANALINK_WS_URL;
256
+ if (!isValidUrl(this.apiUrl, ["http", "https"])) throw new InvalidConfigError(`Invalid apiUrl: "${this.apiUrl}". Must use http or https.`);
257
+ if (!isValidUrl(this.wsUrl, ["ws", "wss"])) throw new InvalidConfigError(`Invalid wsUrl: "${this.wsUrl}". Must use ws or wss.`);
258
+ this.dapp = config.dapp;
259
+ this.jwks = createBananalinkJwks(this.apiUrl);
260
+ }
261
+ async connect(opts) {
262
+ const rawJwt = opts?.jwt;
263
+ const jwtPayload = await (rawJwt ? this.getJwtPayload(rawJwt) : Promise.resolve(null));
264
+ if (!jwtPayload || !rawJwt) {
265
+ const { dappInstanceId, nonce } = await this.connectDappInstance();
266
+ return new BananalinkConnection({
267
+ jwt: void 0,
268
+ dappId: this.dapp.dappId,
269
+ dappInstanceId,
270
+ nonce,
271
+ wsUrl: this.wsUrl
272
+ });
273
+ }
274
+ return new BananalinkConnection({
275
+ jwt: {
276
+ jwtPayload,
277
+ rawJwt
278
+ },
279
+ dappId: this.dapp.dappId,
280
+ dappInstanceId: null,
281
+ nonce: null,
282
+ wsUrl: this.wsUrl
283
+ });
284
+ }
285
+ async getJwtPayload(rawJwt) {
286
+ return await verifyJwt(rawJwt, this.jwks);
287
+ }
288
+ async connectDappInstance() {
289
+ return await createDappInstance(this.apiUrl, this.dapp);
290
+ }
291
+ };
292
+
293
+ //#endregion
294
+ //#region src/errors/InvalidUrlError.ts
295
+ var InvalidUrlError = class extends BananalinkError {
296
+ constructor(url) {
297
+ super(`${url} is not a valid url`);
298
+ this.url = url;
299
+ }
300
+ };
301
+
302
+ //#endregion
303
+ //#region src/utils/displayBananalinkQR.ts
304
+ async function displayBananalinkQR(url) {
305
+ if (!isValidUrl(url, ["bananalink"])) throw new InvalidUrlError(url);
306
+ return qrcode.default.toDataURL(url);
307
+ }
308
+
309
+ //#endregion
310
+ exports.BananalinkClient = BananalinkClient;
311
+ exports.BananalinkConnection = BananalinkConnection;
312
+ exports.BananalinkError = BananalinkError;
313
+ exports.BananalinkSession = BananalinkSession;
314
+ exports.ConnectionRejectedError = ConnectionRejectedError;
315
+ exports.ConnectionTimeoutError = ConnectionTimeoutError;
316
+ exports.DappApiError = DappApiError;
317
+ exports.InvalidConfigError = InvalidConfigError;
318
+ exports.InvalidConnectionStateError = InvalidConnectionStateError;
319
+ exports.displayBananalinkQR = displayBananalinkQR;
@@ -0,0 +1,113 @@
1
+ //#region src/types/Dapp.d.ts
2
+ type Dapp = {
3
+ dappId: string;
4
+ dappName: string;
5
+ dappDescription?: string;
6
+ dappStatement: string;
7
+ domain: string;
8
+ uri: string;
9
+ icons?: string[];
10
+ chainId?: number;
11
+ };
12
+ //#endregion
13
+ //#region src/types/JwtPayload.d.ts
14
+ type JwtPayload = {
15
+ address: string;
16
+ dappId: string;
17
+ };
18
+ //#endregion
19
+ //#region src/core/BananalinkSession.d.ts
20
+ declare class BananalinkSession {
21
+ private readonly ws;
22
+ private static readonly PING_INTERVAL_MILLIS;
23
+ private static readonly PONG_TIMEOUT_MILLIS;
24
+ private pingTimer;
25
+ private pongTimer;
26
+ constructor(ws: WebSocket);
27
+ close(): void;
28
+ private startPing;
29
+ private ping;
30
+ private stopTimers;
31
+ }
32
+ //#endregion
33
+ //#region src/core/BananalinkConnection.d.ts
34
+ type BananalinkDappJwt = {
35
+ jwtPayload: JwtPayload;
36
+ rawJwt: string;
37
+ } | undefined;
38
+ declare class BananalinkConnection {
39
+ private static readonly AUTH_TIMEOUT_MILLIS;
40
+ private readonly wsUrl;
41
+ private readonly jwt;
42
+ private readonly dappId;
43
+ private readonly dappInstanceId;
44
+ private readonly nonce;
45
+ constructor(opts: {
46
+ wsUrl: string;
47
+ jwt: BananalinkDappJwt;
48
+ dappId: string;
49
+ dappInstanceId: string | null;
50
+ nonce: string | null;
51
+ });
52
+ getSession(): Promise<BananalinkSession>;
53
+ get connectionUrl(): string;
54
+ get authorized(): boolean;
55
+ private static bind;
56
+ }
57
+ //#endregion
58
+ //#region src/core/BananalinkClient.d.ts
59
+ declare class BananalinkClient {
60
+ private readonly apiUrl;
61
+ private readonly wsUrl;
62
+ private readonly jwks;
63
+ private readonly dapp;
64
+ private static readonly DEFAULT_BANANALINK_API_URL;
65
+ private static readonly DEFAULT_BANANALINK_WS_URL;
66
+ constructor(config: {
67
+ apiUrl?: string;
68
+ wsUrl?: string;
69
+ dapp: Dapp;
70
+ });
71
+ connect(opts?: {
72
+ jwt: string;
73
+ }): Promise<BananalinkConnection>;
74
+ private getJwtPayload;
75
+ private connectDappInstance;
76
+ }
77
+ //#endregion
78
+ //#region src/errors/BananalinkError.d.ts
79
+ declare class BananalinkError extends Error {
80
+ constructor(message: string, options?: ErrorOptions);
81
+ }
82
+ //#endregion
83
+ //#region src/errors/ConnectionRejectedError.d.ts
84
+ declare class ConnectionRejectedError extends BananalinkError {
85
+ constructor();
86
+ }
87
+ //#endregion
88
+ //#region src/errors/ConnectionTimeoutError.d.ts
89
+ declare class ConnectionTimeoutError extends BananalinkError {
90
+ constructor();
91
+ }
92
+ //#endregion
93
+ //#region src/errors/DappApiError.d.ts
94
+ declare class DappApiError extends BananalinkError {
95
+ readonly statusCode: number;
96
+ constructor(message: string, statusCode: number);
97
+ }
98
+ //#endregion
99
+ //#region src/errors/InvalidConfigError.d.ts
100
+ declare class InvalidConfigError extends BananalinkError {
101
+ constructor(message: string);
102
+ }
103
+ //#endregion
104
+ //#region src/errors/InvalidConnectionStateError.d.ts
105
+ declare class InvalidConnectionStateError extends BananalinkError {
106
+ constructor(message: string);
107
+ }
108
+ //#endregion
109
+ //#region src/utils/displayBananalinkQR.d.ts
110
+ declare function displayBananalinkQR(url: string): Promise<string>;
111
+ //#endregion
112
+ export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, DappApiError, InvalidConfigError, InvalidConnectionStateError, displayBananalinkQR };
113
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types/Dapp.ts","../src/types/JwtPayload.ts","../src/core/BananalinkSession.ts","../src/core/BananalinkConnection.ts","../src/core/BananalinkClient.ts","../src/errors/BananalinkError.ts","../src/errors/ConnectionRejectedError.ts","../src/errors/ConnectionTimeoutError.ts","../src/errors/DappApiError.ts","../src/errors/InvalidConfigError.ts","../src/errors/InvalidConnectionStateError.ts","../src/utils/displayBananalinkQR.ts"],"mappings":";KAAY,IAAA;EACV,MAAA;EACA,QAAA;EACA,eAAA;EACA,aAAA;EACA,MAAA;EACA,GAAA;EACA,KAAA;EACA,OAAA;AAAA;;;KCRU,UAAA;EAAe,OAAA;EAAiB,MAAA;AAAA;;;cCA/B,iBAAA;EAAA,iBAOkB,EAAA;EAAA,wBANL,oBAAA;EAAA,wBACA,mBAAA;EAAA,QAEhB,SAAA;EAAA,QACA,SAAA;cAEqB,EAAA,EAAI,SAAA;EAW1B,KAAA,CAAA;EAAA,QAKC,SAAA;EAAA,QAIA,IAAA;EAAA,QASA,UAAA;AAAA;;;KC7BL,iBAAA;EAAsB,UAAA,EAAY,UAAA;EAAY,MAAA;AAAA;AAAA,cAEtC,oBAAA;EAAA,wBACa,mBAAA;EAAA,iBACP,KAAA;EAAA,iBACA,GAAA;EAAA,iBACA,MAAA;EAAA,iBACA,cAAA;EAAA,iBACA,KAAA;cAEL,IAAA;IACV,KAAA;IACA,GAAA,EAAK,iBAAA;IACL,MAAA;IACA,cAAA;IACA,KAAA;EAAA;EASW,UAAA,CAAA,GAAc,OAAA,CAAQ,iBAAA;EAAA,IA2CxB,aAAA,CAAA;EAAA,IASA,UAAA,CAAA;EAAA,eAKI,IAAA;AAAA;;;cC9EJ,gBAAA;EAAA,iBACM,MAAA;EAAA,iBACA,KAAA;EAAA,iBACA,IAAA;EAAA,iBACA,IAAA;EAAA,wBACO,0BAAA;EAAA,wBACA,yBAAA;cAEZ,MAAA;IACV,MAAA;IACA,KAAA;IACA,IAAA,EAAM,IAAA;EAAA;EAcK,OAAA,CAAQ,IAAA;IAAS,GAAA;EAAA,IAAgB,OAAA,CAAQ,oBAAA;EAAA,QAsBxC,aAAA;EAAA,QAIA,mBAAA;AAAA;;;cC7DH,eAAA,SAAwB,KAAA;cACvB,OAAA,UAAiB,OAAA,GAAU,YAAA;AAAA;;;cCC5B,uBAAA,SAAgC,eAAA;EAAA,WAAA,CAAA;AAAA;;;cCAhC,sBAAA,SAA+B,eAAA;EAAA,WAAA,CAAA;AAAA;;;cCA/B,YAAA,SAAqB,eAAA;EAAA,SAGd,UAAA;cADhB,OAAA,UACgB,UAAA;AAAA;;;cCHP,kBAAA,SAA2B,eAAA;cAC1B,OAAA;AAAA;;;cCDD,2BAAA,SAAoC,eAAA;cACnC,OAAA;AAAA;;;iBCCQ,mBAAA,CAAoB,GAAA,WAAc,OAAA"}
@@ -0,0 +1,113 @@
1
+ //#region src/types/Dapp.d.ts
2
+ type Dapp = {
3
+ dappId: string;
4
+ dappName: string;
5
+ dappDescription?: string;
6
+ dappStatement: string;
7
+ domain: string;
8
+ uri: string;
9
+ icons?: string[];
10
+ chainId?: number;
11
+ };
12
+ //#endregion
13
+ //#region src/types/JwtPayload.d.ts
14
+ type JwtPayload = {
15
+ address: string;
16
+ dappId: string;
17
+ };
18
+ //#endregion
19
+ //#region src/core/BananalinkSession.d.ts
20
+ declare class BananalinkSession {
21
+ private readonly ws;
22
+ private static readonly PING_INTERVAL_MILLIS;
23
+ private static readonly PONG_TIMEOUT_MILLIS;
24
+ private pingTimer;
25
+ private pongTimer;
26
+ constructor(ws: WebSocket);
27
+ close(): void;
28
+ private startPing;
29
+ private ping;
30
+ private stopTimers;
31
+ }
32
+ //#endregion
33
+ //#region src/core/BananalinkConnection.d.ts
34
+ type BananalinkDappJwt = {
35
+ jwtPayload: JwtPayload;
36
+ rawJwt: string;
37
+ } | undefined;
38
+ declare class BananalinkConnection {
39
+ private static readonly AUTH_TIMEOUT_MILLIS;
40
+ private readonly wsUrl;
41
+ private readonly jwt;
42
+ private readonly dappId;
43
+ private readonly dappInstanceId;
44
+ private readonly nonce;
45
+ constructor(opts: {
46
+ wsUrl: string;
47
+ jwt: BananalinkDappJwt;
48
+ dappId: string;
49
+ dappInstanceId: string | null;
50
+ nonce: string | null;
51
+ });
52
+ getSession(): Promise<BananalinkSession>;
53
+ get connectionUrl(): string;
54
+ get authorized(): boolean;
55
+ private static bind;
56
+ }
57
+ //#endregion
58
+ //#region src/core/BananalinkClient.d.ts
59
+ declare class BananalinkClient {
60
+ private readonly apiUrl;
61
+ private readonly wsUrl;
62
+ private readonly jwks;
63
+ private readonly dapp;
64
+ private static readonly DEFAULT_BANANALINK_API_URL;
65
+ private static readonly DEFAULT_BANANALINK_WS_URL;
66
+ constructor(config: {
67
+ apiUrl?: string;
68
+ wsUrl?: string;
69
+ dapp: Dapp;
70
+ });
71
+ connect(opts?: {
72
+ jwt: string;
73
+ }): Promise<BananalinkConnection>;
74
+ private getJwtPayload;
75
+ private connectDappInstance;
76
+ }
77
+ //#endregion
78
+ //#region src/errors/BananalinkError.d.ts
79
+ declare class BananalinkError extends Error {
80
+ constructor(message: string, options?: ErrorOptions);
81
+ }
82
+ //#endregion
83
+ //#region src/errors/ConnectionRejectedError.d.ts
84
+ declare class ConnectionRejectedError extends BananalinkError {
85
+ constructor();
86
+ }
87
+ //#endregion
88
+ //#region src/errors/ConnectionTimeoutError.d.ts
89
+ declare class ConnectionTimeoutError extends BananalinkError {
90
+ constructor();
91
+ }
92
+ //#endregion
93
+ //#region src/errors/DappApiError.d.ts
94
+ declare class DappApiError extends BananalinkError {
95
+ readonly statusCode: number;
96
+ constructor(message: string, statusCode: number);
97
+ }
98
+ //#endregion
99
+ //#region src/errors/InvalidConfigError.d.ts
100
+ declare class InvalidConfigError extends BananalinkError {
101
+ constructor(message: string);
102
+ }
103
+ //#endregion
104
+ //#region src/errors/InvalidConnectionStateError.d.ts
105
+ declare class InvalidConnectionStateError extends BananalinkError {
106
+ constructor(message: string);
107
+ }
108
+ //#endregion
109
+ //#region src/utils/displayBananalinkQR.d.ts
110
+ declare function displayBananalinkQR(url: string): Promise<string>;
111
+ //#endregion
112
+ export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, DappApiError, InvalidConfigError, InvalidConnectionStateError, displayBananalinkQR };
113
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/Dapp.ts","../src/types/JwtPayload.ts","../src/core/BananalinkSession.ts","../src/core/BananalinkConnection.ts","../src/core/BananalinkClient.ts","../src/errors/BananalinkError.ts","../src/errors/ConnectionRejectedError.ts","../src/errors/ConnectionTimeoutError.ts","../src/errors/DappApiError.ts","../src/errors/InvalidConfigError.ts","../src/errors/InvalidConnectionStateError.ts","../src/utils/displayBananalinkQR.ts"],"mappings":";KAAY,IAAA;EACV,MAAA;EACA,QAAA;EACA,eAAA;EACA,aAAA;EACA,MAAA;EACA,GAAA;EACA,KAAA;EACA,OAAA;AAAA;;;KCRU,UAAA;EAAe,OAAA;EAAiB,MAAA;AAAA;;;cCA/B,iBAAA;EAAA,iBAOkB,EAAA;EAAA,wBANL,oBAAA;EAAA,wBACA,mBAAA;EAAA,QAEhB,SAAA;EAAA,QACA,SAAA;cAEqB,EAAA,EAAI,SAAA;EAW1B,KAAA,CAAA;EAAA,QAKC,SAAA;EAAA,QAIA,IAAA;EAAA,QASA,UAAA;AAAA;;;KC7BL,iBAAA;EAAsB,UAAA,EAAY,UAAA;EAAY,MAAA;AAAA;AAAA,cAEtC,oBAAA;EAAA,wBACa,mBAAA;EAAA,iBACP,KAAA;EAAA,iBACA,GAAA;EAAA,iBACA,MAAA;EAAA,iBACA,cAAA;EAAA,iBACA,KAAA;cAEL,IAAA;IACV,KAAA;IACA,GAAA,EAAK,iBAAA;IACL,MAAA;IACA,cAAA;IACA,KAAA;EAAA;EASW,UAAA,CAAA,GAAc,OAAA,CAAQ,iBAAA;EAAA,IA2CxB,aAAA,CAAA;EAAA,IASA,UAAA,CAAA;EAAA,eAKI,IAAA;AAAA;;;cC9EJ,gBAAA;EAAA,iBACM,MAAA;EAAA,iBACA,KAAA;EAAA,iBACA,IAAA;EAAA,iBACA,IAAA;EAAA,wBACO,0BAAA;EAAA,wBACA,yBAAA;cAEZ,MAAA;IACV,MAAA;IACA,KAAA;IACA,IAAA,EAAM,IAAA;EAAA;EAcK,OAAA,CAAQ,IAAA;IAAS,GAAA;EAAA,IAAgB,OAAA,CAAQ,oBAAA;EAAA,QAsBxC,aAAA;EAAA,QAIA,mBAAA;AAAA;;;cC7DH,eAAA,SAAwB,KAAA;cACvB,OAAA,UAAiB,OAAA,GAAU,YAAA;AAAA;;;cCC5B,uBAAA,SAAgC,eAAA;EAAA,WAAA,CAAA;AAAA;;;cCAhC,sBAAA,SAA+B,eAAA;EAAA,WAAA,CAAA;AAAA;;;cCA/B,YAAA,SAAqB,eAAA;EAAA,SAGd,UAAA;cADhB,OAAA,UACgB,UAAA;AAAA;;;cCHP,kBAAA,SAA2B,eAAA;cAC1B,OAAA;AAAA;;;cCDD,2BAAA,SAAoC,eAAA;cACnC,OAAA;AAAA;;;iBCCQ,mBAAA,CAAoB,GAAA,WAAc,OAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,282 @@
1
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2
+ import QRCode from "qrcode";
3
+
4
+ //#region src/errors/BananalinkError.ts
5
+ var BananalinkError = class extends Error {
6
+ constructor(message, options) {
7
+ super(message, options);
8
+ this.name = "BananalinkError";
9
+ }
10
+ };
11
+
12
+ //#endregion
13
+ //#region src/errors/DappApiError.ts
14
+ var DappApiError = class extends BananalinkError {
15
+ constructor(message, statusCode) {
16
+ super(message);
17
+ this.statusCode = statusCode;
18
+ this.name = "DappApiError";
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ //#region src/api/createDappInstance.ts
24
+ function isCreateDappInstanceResponse(body) {
25
+ return typeof body === "object" && body !== null && "dappInstanceId" in body && typeof body.dappInstanceId === "string" && "nonce" in body && typeof body.nonce === "string";
26
+ }
27
+ async function createDappInstance(apiUrl, params) {
28
+ const response = await fetch(`${apiUrl}/dapps`, {
29
+ headers: { "Content-Type": "application/json" },
30
+ method: "POST",
31
+ body: JSON.stringify(params)
32
+ });
33
+ if (response.ok) {
34
+ const body = await response.json().catch(() => {
35
+ throw new DappApiError("Invalid JSON response from bananalink API", response.status);
36
+ });
37
+ if (!isCreateDappInstanceResponse(body)) throw new DappApiError("Unexpected response shape from bananalink API", response.status);
38
+ return body;
39
+ }
40
+ throw new DappApiError("Failed to create dapp instance", response.status);
41
+ }
42
+
43
+ //#endregion
44
+ //#region src/errors/InvalidConfigError.ts
45
+ var InvalidConfigError = class extends BananalinkError {
46
+ constructor(message) {
47
+ super(message);
48
+ this.name = "InvalidConfigError";
49
+ }
50
+ };
51
+
52
+ //#endregion
53
+ //#region src/jwt/createBananalinkJwks.ts
54
+ function createBananalinkJwks(apiUrl) {
55
+ return createRemoteJWKSet(new URL(`${apiUrl}/.well-known/jwks.json`));
56
+ }
57
+
58
+ //#endregion
59
+ //#region src/jwt/verifyJwt.ts
60
+ async function verifyJwt(jwt, jwks) {
61
+ try {
62
+ const { payload } = await jwtVerify(jwt, jwks, {
63
+ algorithms: ["ES256"],
64
+ issuer: "bananalink",
65
+ requiredClaims: ["sub", "aud"]
66
+ });
67
+ return {
68
+ address: payload.sub,
69
+ dappId: Array.isArray(payload.aud) ? payload.aud[0] : payload.aud
70
+ };
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ //#endregion
77
+ //#region src/utils/isValidUrl.ts
78
+ function isValidUrl(url, protocols) {
79
+ try {
80
+ const parsed = new URL(url);
81
+ return protocols.includes(parsed.protocol.replace(":", ""));
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/errors/ConnectionRejectedError.ts
89
+ var ConnectionRejectedError = class extends BananalinkError {
90
+ constructor() {
91
+ super("Dapp connection attempt was rejected by the wallet");
92
+ this.name = "ConnectionRejectedError";
93
+ }
94
+ };
95
+
96
+ //#endregion
97
+ //#region src/errors/ConnectionTimeoutError.ts
98
+ var ConnectionTimeoutError = class extends BananalinkError {
99
+ constructor() {
100
+ super("Dapp connection attempt timed out");
101
+ this.name = "ConnectionTimeoutError";
102
+ }
103
+ };
104
+
105
+ //#endregion
106
+ //#region src/errors/InvalidConnectionStateError.ts
107
+ var InvalidConnectionStateError = class extends BananalinkError {
108
+ constructor(message) {
109
+ super(message);
110
+ this.name = "InvalidConnectionStateError";
111
+ }
112
+ };
113
+
114
+ //#endregion
115
+ //#region src/core/BananalinkSession.ts
116
+ var BananalinkSession = class BananalinkSession {
117
+ static PING_INTERVAL_MILLIS = 30 * 1e3;
118
+ static PONG_TIMEOUT_MILLIS = 5 * 1e3;
119
+ pingTimer = null;
120
+ pongTimer = null;
121
+ constructor(ws) {
122
+ this.ws = ws;
123
+ this.ws.addEventListener("message", (event) => {
124
+ const { type } = JSON.parse(event.data);
125
+ if (type === "pong" && this.pongTimer) {
126
+ clearTimeout(this.pongTimer);
127
+ this.pongTimer = null;
128
+ }
129
+ });
130
+ this.startPing();
131
+ }
132
+ close() {
133
+ this.stopTimers();
134
+ this.ws.close();
135
+ }
136
+ startPing() {
137
+ this.pingTimer = setInterval(() => this.ping(), BananalinkSession.PING_INTERVAL_MILLIS);
138
+ }
139
+ ping() {
140
+ if (this.ws.readyState !== WebSocket.OPEN) {
141
+ this.close();
142
+ return;
143
+ }
144
+ this.ws.send(JSON.stringify({ type: "ping" }));
145
+ this.pongTimer = setTimeout(() => this.close(), BananalinkSession.PONG_TIMEOUT_MILLIS);
146
+ }
147
+ stopTimers() {
148
+ if (this.pingTimer !== null) clearInterval(this.pingTimer);
149
+ if (this.pongTimer !== null) clearTimeout(this.pongTimer);
150
+ this.pingTimer = null;
151
+ this.pongTimer = null;
152
+ }
153
+ };
154
+
155
+ //#endregion
156
+ //#region src/core/BananalinkConnection.ts
157
+ var BananalinkConnection = class BananalinkConnection {
158
+ static AUTH_TIMEOUT_MILLIS = 60 * 1e3;
159
+ wsUrl;
160
+ jwt;
161
+ dappId;
162
+ dappInstanceId;
163
+ nonce;
164
+ constructor(opts) {
165
+ this.wsUrl = opts.wsUrl;
166
+ this.jwt = opts.jwt;
167
+ this.dappId = opts.dappId;
168
+ this.dappInstanceId = opts.dappInstanceId;
169
+ this.nonce = opts.nonce;
170
+ }
171
+ async getSession() {
172
+ if (this.jwt) throw new BananalinkError("Recovering a lost dapp connection is not yet implemented");
173
+ const dappInstanceId = this.dappInstanceId;
174
+ if (!dappInstanceId) throw new InvalidConnectionStateError("Cannot get session without authorizing the dapp instance or providing a valid JWT");
175
+ return new Promise((resolve, reject) => {
176
+ const ws = new WebSocket(`${this.wsUrl}?dappInstanceId=${encodeURIComponent(dappInstanceId)}`);
177
+ const authTimeout = setTimeout(() => {
178
+ ws.close();
179
+ reject(new ConnectionTimeoutError());
180
+ }, BananalinkConnection.AUTH_TIMEOUT_MILLIS);
181
+ const onMessage = (event) => {
182
+ const msg = JSON.parse(event.data);
183
+ clearTimeout(authTimeout);
184
+ ws.removeEventListener("message", onMessage);
185
+ if (msg.type === "authorized") {
186
+ BananalinkConnection.bind(ws, msg.accessToken);
187
+ resolve(new BananalinkSession(ws));
188
+ } else if (msg.type === "rejected") {
189
+ ws.close();
190
+ reject(new ConnectionRejectedError());
191
+ }
192
+ };
193
+ ws.addEventListener("message", onMessage);
194
+ ws.addEventListener("error", () => {});
195
+ ws.addEventListener("close", () => {});
196
+ });
197
+ }
198
+ get connectionUrl() {
199
+ if (!this.dappInstanceId || !this.nonce) throw new InvalidConnectionStateError("Cannot get connection URL without a dapp instance");
200
+ const dappInstanceId = this.dappInstanceId;
201
+ const nonce = this.nonce;
202
+ return `bananalink://?dappId=${encodeURIComponent(this.dappId)}&dappInstanceId=${encodeURIComponent(dappInstanceId)}&nonce=${encodeURIComponent(nonce)}`;
203
+ }
204
+ get authorized() {
205
+ return this.jwt !== void 0;
206
+ }
207
+ static bind(ws, jwt) {
208
+ ws.send(JSON.stringify({
209
+ type: "bind",
210
+ jwt
211
+ }));
212
+ }
213
+ };
214
+
215
+ //#endregion
216
+ //#region src/core/BananalinkClient.ts
217
+ var BananalinkClient = class BananalinkClient {
218
+ apiUrl;
219
+ wsUrl;
220
+ jwks;
221
+ dapp;
222
+ static DEFAULT_BANANALINK_API_URL = "https://tfr9p2mn4i.execute-api.us-east-2.amazonaws.com";
223
+ static DEFAULT_BANANALINK_WS_URL = "wss://yb47qomkt3.execute-api.us-east-2.amazonaws.com/v1";
224
+ constructor(config) {
225
+ this.apiUrl = config.apiUrl ?? BananalinkClient.DEFAULT_BANANALINK_API_URL;
226
+ this.wsUrl = config.wsUrl ?? BananalinkClient.DEFAULT_BANANALINK_WS_URL;
227
+ if (!isValidUrl(this.apiUrl, ["http", "https"])) throw new InvalidConfigError(`Invalid apiUrl: "${this.apiUrl}". Must use http or https.`);
228
+ if (!isValidUrl(this.wsUrl, ["ws", "wss"])) throw new InvalidConfigError(`Invalid wsUrl: "${this.wsUrl}". Must use ws or wss.`);
229
+ this.dapp = config.dapp;
230
+ this.jwks = createBananalinkJwks(this.apiUrl);
231
+ }
232
+ async connect(opts) {
233
+ const rawJwt = opts?.jwt;
234
+ const jwtPayload = await (rawJwt ? this.getJwtPayload(rawJwt) : Promise.resolve(null));
235
+ if (!jwtPayload || !rawJwt) {
236
+ const { dappInstanceId, nonce } = await this.connectDappInstance();
237
+ return new BananalinkConnection({
238
+ jwt: void 0,
239
+ dappId: this.dapp.dappId,
240
+ dappInstanceId,
241
+ nonce,
242
+ wsUrl: this.wsUrl
243
+ });
244
+ }
245
+ return new BananalinkConnection({
246
+ jwt: {
247
+ jwtPayload,
248
+ rawJwt
249
+ },
250
+ dappId: this.dapp.dappId,
251
+ dappInstanceId: null,
252
+ nonce: null,
253
+ wsUrl: this.wsUrl
254
+ });
255
+ }
256
+ async getJwtPayload(rawJwt) {
257
+ return await verifyJwt(rawJwt, this.jwks);
258
+ }
259
+ async connectDappInstance() {
260
+ return await createDappInstance(this.apiUrl, this.dapp);
261
+ }
262
+ };
263
+
264
+ //#endregion
265
+ //#region src/errors/InvalidUrlError.ts
266
+ var InvalidUrlError = class extends BananalinkError {
267
+ constructor(url) {
268
+ super(`${url} is not a valid url`);
269
+ this.url = url;
270
+ }
271
+ };
272
+
273
+ //#endregion
274
+ //#region src/utils/displayBananalinkQR.ts
275
+ async function displayBananalinkQR(url) {
276
+ if (!isValidUrl(url, ["bananalink"])) throw new InvalidUrlError(url);
277
+ return QRCode.toDataURL(url);
278
+ }
279
+
280
+ //#endregion
281
+ export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, DappApiError, InvalidConfigError, InvalidConnectionStateError, displayBananalinkQR };
282
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/errors/BananalinkError.ts","../src/errors/DappApiError.ts","../src/api/createDappInstance.ts","../src/errors/InvalidConfigError.ts","../src/jwt/createBananalinkJwks.ts","../src/jwt/verifyJwt.ts","../src/utils/isValidUrl.ts","../src/errors/ConnectionRejectedError.ts","../src/errors/ConnectionTimeoutError.ts","../src/errors/InvalidConnectionStateError.ts","../src/core/BananalinkSession.ts","../src/core/BananalinkConnection.ts","../src/core/BananalinkClient.ts","../src/errors/InvalidUrlError.ts","../src/utils/displayBananalinkQR.ts"],"sourcesContent":["export class BananalinkError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'BananalinkError';\n }\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class DappApiError extends BananalinkError {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message);\n this.name = 'DappApiError';\n }\n}\n","import { DappApiError } from '../errors/DappApiError.js';\nimport type { CreateDappInstanceResponse } from '../types/CreateDappInstanceResponse.js';\nimport type { Dapp } from '../types/Dapp.js';\n\nfunction isCreateDappInstanceResponse(body: unknown): body is CreateDappInstanceResponse {\n return (\n typeof body === 'object' &&\n body !== null &&\n 'dappInstanceId' in body &&\n typeof body.dappInstanceId === 'string' &&\n 'nonce' in body &&\n typeof body.nonce === 'string'\n );\n}\n\nexport async function createDappInstance(apiUrl: string, params: Dapp): Promise<CreateDappInstanceResponse> {\n const response = await fetch(`${apiUrl}/dapps`, {\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n body: JSON.stringify(params),\n });\n if (response.ok) {\n const body = await response.json().catch(() => {\n throw new DappApiError('Invalid JSON response from bananalink API', response.status);\n });\n if (!isCreateDappInstanceResponse(body)) {\n throw new DappApiError('Unexpected response shape from bananalink API', response.status);\n }\n return body;\n }\n throw new DappApiError('Failed to create dapp instance', response.status);\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class InvalidConfigError extends BananalinkError {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidConfigError';\n }\n}\n","import { createRemoteJWKSet, type JWTVerifyGetKey } from 'jose';\n\nexport function createBananalinkJwks(apiUrl: string): JWTVerifyGetKey {\n const url = new URL(`${apiUrl}/.well-known/jwks.json`);\n return createRemoteJWKSet(url);\n}\n","import { type JWTVerifyGetKey, jwtVerify } from 'jose';\nimport type { JwtPayload } from '../types/JwtPayload.js';\n\nexport async function verifyJwt(jwt: string, jwks: JWTVerifyGetKey): Promise<JwtPayload | null> {\n try {\n const { payload } = await jwtVerify(jwt, jwks, {\n algorithms: ['ES256'],\n issuer: 'bananalink',\n requiredClaims: ['sub', 'aud'],\n });\n return {\n address: payload.sub as string,\n dappId: Array.isArray(payload.aud) ? payload.aud[0] : (payload.aud as string),\n };\n } catch {\n return null;\n }\n}\n","export function isValidUrl(url: string, protocols: string[]): boolean {\n try {\n const parsed = new URL(url);\n return protocols.includes(parsed.protocol.replace(':', ''));\n } catch {\n return false;\n }\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class ConnectionRejectedError extends BananalinkError {\n constructor() {\n super('Dapp connection attempt was rejected by the wallet');\n this.name = 'ConnectionRejectedError';\n }\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class ConnectionTimeoutError extends BananalinkError {\n constructor() {\n super('Dapp connection attempt timed out');\n this.name = 'ConnectionTimeoutError';\n }\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class InvalidConnectionStateError extends BananalinkError {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidConnectionStateError';\n }\n}\n","export class BananalinkSession {\n private static readonly PING_INTERVAL_MILLIS = 30 * 1000;\n private static readonly PONG_TIMEOUT_MILLIS = 5 * 1000;\n\n private pingTimer: ReturnType<typeof setInterval> | null = null;\n private pongTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(private readonly ws: WebSocket) {\n this.ws.addEventListener('message', (event) => {\n const { type } = JSON.parse(event.data);\n if (type === 'pong' && this.pongTimer) {\n clearTimeout(this.pongTimer);\n this.pongTimer = null;\n }\n });\n this.startPing();\n }\n\n public close(): void {\n this.stopTimers();\n this.ws.close();\n }\n\n private startPing(): void {\n this.pingTimer = setInterval(() => this.ping(), BananalinkSession.PING_INTERVAL_MILLIS);\n }\n\n private ping(): void {\n if (this.ws.readyState !== WebSocket.OPEN) {\n this.close();\n return;\n }\n this.ws.send(JSON.stringify({ type: 'ping' }));\n this.pongTimer = setTimeout(() => this.close(), BananalinkSession.PONG_TIMEOUT_MILLIS);\n }\n\n private stopTimers(): void {\n if (this.pingTimer !== null) {\n clearInterval(this.pingTimer);\n }\n if (this.pongTimer !== null) {\n clearTimeout(this.pongTimer);\n }\n this.pingTimer = null;\n this.pongTimer = null;\n }\n}\n","import { BananalinkError } from '../errors/BananalinkError.js';\nimport { ConnectionRejectedError } from '../errors/ConnectionRejectedError.js';\nimport { ConnectionTimeoutError } from '../errors/ConnectionTimeoutError.js';\nimport { InvalidConnectionStateError } from '../errors/InvalidConnectionStateError.js';\nimport type { JwtPayload } from '../types/JwtPayload.js';\nimport { BananalinkSession } from './BananalinkSession.js';\n\ntype BananalinkDappJwt = { jwtPayload: JwtPayload; rawJwt: string } | undefined;\n\nexport class BananalinkConnection {\n private static readonly AUTH_TIMEOUT_MILLIS = 60 * 1000;\n private readonly wsUrl: string;\n private readonly jwt: BananalinkDappJwt;\n private readonly dappId: string;\n private readonly dappInstanceId: string | null;\n private readonly nonce: string | null;\n\n constructor(opts: {\n wsUrl: string;\n jwt: BananalinkDappJwt;\n dappId: string;\n dappInstanceId: string | null;\n nonce: string | null;\n }) {\n this.wsUrl = opts.wsUrl;\n this.jwt = opts.jwt;\n this.dappId = opts.dappId;\n this.dappInstanceId = opts.dappInstanceId;\n this.nonce = opts.nonce;\n }\n\n public async getSession(): Promise<BananalinkSession> {\n if (this.jwt) {\n throw new BananalinkError('Recovering a lost dapp connection is not yet implemented');\n }\n\n const dappInstanceId = this.dappInstanceId;\n if (!dappInstanceId) {\n throw new InvalidConnectionStateError(\n 'Cannot get session without authorizing the dapp instance or providing a valid JWT',\n );\n }\n\n return new Promise((resolve, reject) => {\n const ws = new WebSocket(`${this.wsUrl}?dappInstanceId=${encodeURIComponent(dappInstanceId)}`);\n\n const authTimeout = setTimeout(() => {\n ws.close();\n reject(new ConnectionTimeoutError());\n }, BananalinkConnection.AUTH_TIMEOUT_MILLIS);\n\n const onMessage = (event: MessageEvent): void => {\n const msg = JSON.parse(event.data);\n\n // cheating, but we don't expect any other type of message at this point\n clearTimeout(authTimeout);\n ws.removeEventListener('message', onMessage);\n\n if (msg.type === 'authorized') {\n BananalinkConnection.bind(ws, msg.accessToken);\n resolve(new BananalinkSession(ws));\n } else if (msg.type === 'rejected') {\n ws.close();\n reject(new ConnectionRejectedError());\n }\n };\n\n // TODO: abstract in a transport interface\n ws.addEventListener('message', onMessage);\n ws.addEventListener('error', () => {});\n ws.addEventListener('close', () => {});\n });\n }\n\n public get connectionUrl(): string {\n if (!this.dappInstanceId || !this.nonce) {\n throw new InvalidConnectionStateError('Cannot get connection URL without a dapp instance');\n }\n const dappInstanceId = this.dappInstanceId;\n const nonce = this.nonce;\n return `bananalink://?dappId=${encodeURIComponent(this.dappId)}&dappInstanceId=${encodeURIComponent(dappInstanceId)}&nonce=${encodeURIComponent(nonce)}`;\n }\n\n public get authorized(): boolean {\n // TODO: handle revocation\n return this.jwt !== undefined;\n }\n\n private static bind(ws: WebSocket, jwt: string) {\n ws.send(JSON.stringify({ type: 'bind', jwt: jwt }));\n }\n}\n","import { createDappInstance } from '../api/createDappInstance.js';\nimport { InvalidConfigError } from '../errors/InvalidConfigError.js';\nimport { createBananalinkJwks } from '../jwt/createBananalinkJwks.js';\nimport { verifyJwt } from '../jwt/verifyJwt.js';\nimport type { CreateDappInstanceResponse } from '../types/CreateDappInstanceResponse.js';\nimport type { Dapp } from '../types/Dapp.js';\nimport type { JwtPayload } from '../types/JwtPayload.js';\nimport { isValidUrl } from '../utils/isValidUrl.js';\nimport { BananalinkConnection } from './BananalinkConnection.js';\n\nexport class BananalinkClient {\n private readonly apiUrl: string;\n private readonly wsUrl: string;\n private readonly jwks: ReturnType<typeof createBananalinkJwks>;\n private readonly dapp: Dapp;\n private static readonly DEFAULT_BANANALINK_API_URL = 'https://tfr9p2mn4i.execute-api.us-east-2.amazonaws.com';\n private static readonly DEFAULT_BANANALINK_WS_URL = 'wss://yb47qomkt3.execute-api.us-east-2.amazonaws.com/v1';\n\n constructor(config: {\n apiUrl?: string;\n wsUrl?: string;\n dapp: Dapp;\n }) {\n this.apiUrl = config.apiUrl ?? BananalinkClient.DEFAULT_BANANALINK_API_URL;\n this.wsUrl = config.wsUrl ?? BananalinkClient.DEFAULT_BANANALINK_WS_URL;\n if (!isValidUrl(this.apiUrl, ['http', 'https'])) {\n throw new InvalidConfigError(`Invalid apiUrl: \"${this.apiUrl}\". Must use http or https.`);\n }\n if (!isValidUrl(this.wsUrl, ['ws', 'wss'])) {\n throw new InvalidConfigError(`Invalid wsUrl: \"${this.wsUrl}\". Must use ws or wss.`);\n }\n this.dapp = config.dapp;\n this.jwks = createBananalinkJwks(this.apiUrl);\n }\n\n public async connect(opts?: { jwt: string }): Promise<BananalinkConnection> {\n const rawJwt = opts?.jwt;\n const jwtPayload = await (rawJwt ? this.getJwtPayload(rawJwt) : Promise.resolve(null));\n if (!jwtPayload || !rawJwt) {\n const { dappInstanceId, nonce } = await this.connectDappInstance();\n return new BananalinkConnection({\n jwt: undefined,\n dappId: this.dapp.dappId,\n dappInstanceId,\n nonce,\n wsUrl: this.wsUrl,\n });\n }\n return new BananalinkConnection({\n jwt: { jwtPayload, rawJwt },\n dappId: this.dapp.dappId,\n dappInstanceId: null,\n nonce: null,\n wsUrl: this.wsUrl,\n });\n }\n\n private async getJwtPayload(rawJwt: string): Promise<JwtPayload | null> {\n return await verifyJwt(rawJwt, this.jwks);\n }\n\n private async connectDappInstance(): Promise<CreateDappInstanceResponse> {\n return await createDappInstance(this.apiUrl, this.dapp);\n }\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class InvalidUrlError extends BananalinkError {\n constructor(public readonly url: string) {\n super(`${url} is not a valid url`);\n }\n}\n","import QRCode from 'qrcode';\nimport { InvalidUrlError } from '../errors/InvalidUrlError.js';\nimport { isValidUrl } from './isValidUrl.js';\n\nexport async function displayBananalinkQR(url: string): Promise<string> {\n if (!isValidUrl(url, ['bananalink'])) {\n throw new InvalidUrlError(url);\n }\n return QRCode.toDataURL(url);\n}\n"],"mappings":";;;;AAAA,IAAa,kBAAb,cAAqC,MAAM;CACzC,YAAY,SAAiB,SAAwB;AACnD,QAAM,SAAS,QAAQ;AACvB,OAAK,OAAO;;;;;;ACDhB,IAAa,eAAb,cAAkC,gBAAgB;CAChD,YACE,SACA,AAAgB,YAChB;AACA,QAAM,QAAQ;EAFE;AAGhB,OAAK,OAAO;;;;;;ACJhB,SAAS,6BAA6B,MAAmD;AACvF,QACE,OAAO,SAAS,YAChB,SAAS,QACT,oBAAoB,QACpB,OAAO,KAAK,mBAAmB,YAC/B,WAAW,QACX,OAAO,KAAK,UAAU;;AAI1B,eAAsB,mBAAmB,QAAgB,QAAmD;CAC1G,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS;EAC9C,SAAS,EACP,gBAAgB,oBACjB;EACD,QAAQ;EACR,MAAM,KAAK,UAAU,OAAO;EAC7B,CAAC;AACF,KAAI,SAAS,IAAI;EACf,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY;AAC7C,SAAM,IAAI,aAAa,6CAA6C,SAAS,OAAO;IACpF;AACF,MAAI,CAAC,6BAA6B,KAAK,CACrC,OAAM,IAAI,aAAa,iDAAiD,SAAS,OAAO;AAE1F,SAAO;;AAET,OAAM,IAAI,aAAa,kCAAkC,SAAS,OAAO;;;;;AC9B3E,IAAa,qBAAb,cAAwC,gBAAgB;CACtD,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;ACHhB,SAAgB,qBAAqB,QAAiC;AAEpE,QAAO,mBADK,IAAI,IAAI,GAAG,OAAO,wBAAwB,CACxB;;;;;ACDhC,eAAsB,UAAU,KAAa,MAAmD;AAC9F,KAAI;EACF,MAAM,EAAE,YAAY,MAAM,UAAU,KAAK,MAAM;GAC7C,YAAY,CAAC,QAAQ;GACrB,QAAQ;GACR,gBAAgB,CAAC,OAAO,MAAM;GAC/B,CAAC;AACF,SAAO;GACL,SAAS,QAAQ;GACjB,QAAQ,MAAM,QAAQ,QAAQ,IAAI,GAAG,QAAQ,IAAI,KAAM,QAAQ;GAChE;SACK;AACN,SAAO;;;;;;ACfX,SAAgB,WAAW,KAAa,WAA8B;AACpE,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,SAAO,UAAU,SAAS,OAAO,SAAS,QAAQ,KAAK,GAAG,CAAC;SACrD;AACN,SAAO;;;;;;ACHX,IAAa,0BAAb,cAA6C,gBAAgB;CAC3D,cAAc;AACZ,QAAM,qDAAqD;AAC3D,OAAK,OAAO;;;;;;ACHhB,IAAa,yBAAb,cAA4C,gBAAgB;CAC1D,cAAc;AACZ,QAAM,oCAAoC;AAC1C,OAAK,OAAO;;;;;;ACHhB,IAAa,8BAAb,cAAiD,gBAAgB;CAC/D,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;ACLhB,IAAa,oBAAb,MAAa,kBAAkB;CAC7B,OAAwB,uBAAuB,KAAK;CACpD,OAAwB,sBAAsB,IAAI;CAElD,AAAQ,YAAmD;CAC3D,AAAQ,YAAkD;CAE1D,YAAY,AAAiB,IAAe;EAAf;AAC3B,OAAK,GAAG,iBAAiB,YAAY,UAAU;GAC7C,MAAM,EAAE,SAAS,KAAK,MAAM,MAAM,KAAK;AACvC,OAAI,SAAS,UAAU,KAAK,WAAW;AACrC,iBAAa,KAAK,UAAU;AAC5B,SAAK,YAAY;;IAEnB;AACF,OAAK,WAAW;;CAGlB,AAAO,QAAc;AACnB,OAAK,YAAY;AACjB,OAAK,GAAG,OAAO;;CAGjB,AAAQ,YAAkB;AACxB,OAAK,YAAY,kBAAkB,KAAK,MAAM,EAAE,kBAAkB,qBAAqB;;CAGzF,AAAQ,OAAa;AACnB,MAAI,KAAK,GAAG,eAAe,UAAU,MAAM;AACzC,QAAK,OAAO;AACZ;;AAEF,OAAK,GAAG,KAAK,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC,CAAC;AAC9C,OAAK,YAAY,iBAAiB,KAAK,OAAO,EAAE,kBAAkB,oBAAoB;;CAGxF,AAAQ,aAAmB;AACzB,MAAI,KAAK,cAAc,KACrB,eAAc,KAAK,UAAU;AAE/B,MAAI,KAAK,cAAc,KACrB,cAAa,KAAK,UAAU;AAE9B,OAAK,YAAY;AACjB,OAAK,YAAY;;;;;;ACnCrB,IAAa,uBAAb,MAAa,qBAAqB;CAChC,OAAwB,sBAAsB,KAAK;CACnD,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,YAAY,MAMT;AACD,OAAK,QAAQ,KAAK;AAClB,OAAK,MAAM,KAAK;AAChB,OAAK,SAAS,KAAK;AACnB,OAAK,iBAAiB,KAAK;AAC3B,OAAK,QAAQ,KAAK;;CAGpB,MAAa,aAAyC;AACpD,MAAI,KAAK,IACP,OAAM,IAAI,gBAAgB,2DAA2D;EAGvF,MAAM,iBAAiB,KAAK;AAC5B,MAAI,CAAC,eACH,OAAM,IAAI,4BACR,oFACD;AAGH,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,KAAK,IAAI,UAAU,GAAG,KAAK,MAAM,kBAAkB,mBAAmB,eAAe,GAAG;GAE9F,MAAM,cAAc,iBAAiB;AACnC,OAAG,OAAO;AACV,WAAO,IAAI,wBAAwB,CAAC;MACnC,qBAAqB,oBAAoB;GAE5C,MAAM,aAAa,UAA8B;IAC/C,MAAM,MAAM,KAAK,MAAM,MAAM,KAAK;AAGlC,iBAAa,YAAY;AACzB,OAAG,oBAAoB,WAAW,UAAU;AAE5C,QAAI,IAAI,SAAS,cAAc;AAC7B,0BAAqB,KAAK,IAAI,IAAI,YAAY;AAC9C,aAAQ,IAAI,kBAAkB,GAAG,CAAC;eACzB,IAAI,SAAS,YAAY;AAClC,QAAG,OAAO;AACV,YAAO,IAAI,yBAAyB,CAAC;;;AAKzC,MAAG,iBAAiB,WAAW,UAAU;AACzC,MAAG,iBAAiB,eAAe,GAAG;AACtC,MAAG,iBAAiB,eAAe,GAAG;IACtC;;CAGJ,IAAW,gBAAwB;AACjC,MAAI,CAAC,KAAK,kBAAkB,CAAC,KAAK,MAChC,OAAM,IAAI,4BAA4B,oDAAoD;EAE5F,MAAM,iBAAiB,KAAK;EAC5B,MAAM,QAAQ,KAAK;AACnB,SAAO,wBAAwB,mBAAmB,KAAK,OAAO,CAAC,kBAAkB,mBAAmB,eAAe,CAAC,SAAS,mBAAmB,MAAM;;CAGxJ,IAAW,aAAsB;AAE/B,SAAO,KAAK,QAAQ;;CAGtB,OAAe,KAAK,IAAe,KAAa;AAC9C,KAAG,KAAK,KAAK,UAAU;GAAE,MAAM;GAAa;GAAK,CAAC,CAAC;;;;;;AC/EvD,IAAa,mBAAb,MAAa,iBAAiB;CAC5B,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,OAAwB,6BAA6B;CACrD,OAAwB,4BAA4B;CAEpD,YAAY,QAIT;AACD,OAAK,SAAS,OAAO,UAAU,iBAAiB;AAChD,OAAK,QAAQ,OAAO,SAAS,iBAAiB;AAC9C,MAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,QAAQ,QAAQ,CAAC,CAC7C,OAAM,IAAI,mBAAmB,oBAAoB,KAAK,OAAO,4BAA4B;AAE3F,MAAI,CAAC,WAAW,KAAK,OAAO,CAAC,MAAM,MAAM,CAAC,CACxC,OAAM,IAAI,mBAAmB,mBAAmB,KAAK,MAAM,wBAAwB;AAErF,OAAK,OAAO,OAAO;AACnB,OAAK,OAAO,qBAAqB,KAAK,OAAO;;CAG/C,MAAa,QAAQ,MAAuD;EAC1E,MAAM,SAAS,MAAM;EACrB,MAAM,aAAa,OAAO,SAAS,KAAK,cAAc,OAAO,GAAG,QAAQ,QAAQ,KAAK;AACrF,MAAI,CAAC,cAAc,CAAC,QAAQ;GAC1B,MAAM,EAAE,gBAAgB,UAAU,MAAM,KAAK,qBAAqB;AAClE,UAAO,IAAI,qBAAqB;IAC9B,KAAK;IACL,QAAQ,KAAK,KAAK;IAClB;IACA;IACA,OAAO,KAAK;IACb,CAAC;;AAEJ,SAAO,IAAI,qBAAqB;GAC9B,KAAK;IAAE;IAAY;IAAQ;GAC3B,QAAQ,KAAK,KAAK;GAClB,gBAAgB;GAChB,OAAO;GACP,OAAO,KAAK;GACb,CAAC;;CAGJ,MAAc,cAAc,QAA4C;AACtE,SAAO,MAAM,UAAU,QAAQ,KAAK,KAAK;;CAG3C,MAAc,sBAA2D;AACvE,SAAO,MAAM,mBAAmB,KAAK,QAAQ,KAAK,KAAK;;;;;;AC5D3D,IAAa,kBAAb,cAAqC,gBAAgB;CACnD,YAAY,AAAgB,KAAa;AACvC,QAAM,GAAG,IAAI,qBAAqB;EADR;;;;;;ACC9B,eAAsB,oBAAoB,KAA8B;AACtE,KAAI,CAAC,WAAW,KAAK,CAAC,aAAa,CAAC,CAClC,OAAM,IAAI,gBAAgB,IAAI;AAEhC,QAAO,OAAO,UAAU,IAAI"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@bananalink-test/client",
3
+ "version": "0.0.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "import": {
10
+ "types": "./dist/index.d.mts",
11
+ "default": "./dist/index.mjs"
12
+ },
13
+ "require": {
14
+ "types": "./dist/index.d.cts",
15
+ "default": "./dist/index.cjs"
16
+ }
17
+ }
18
+ },
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.mts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "devDependencies": {
29
+ "@biomejs/biome": "2.3.15",
30
+ "@types/qrcode": "^1.5.6",
31
+ "tsdown": "^0.20.3",
32
+ "typescript": "^5.9.3"
33
+ },
34
+ "dependencies": {
35
+ "jose": "^6.1.3",
36
+ "qrcode": "^1.5.4"
37
+ },
38
+ "scripts": {
39
+ "build": "tsdown",
40
+ "type-check": "tsc --noEmit",
41
+ "lint": "biome check .",
42
+ "dev": "tsdown --watch"
43
+ }
44
+ }