@bananalink-test/client 0.2.0 → 0.5.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 CHANGED
@@ -79,6 +79,14 @@ var InvalidConfigError = class extends BananalinkError {
79
79
  }
80
80
  };
81
81
 
82
+ //#endregion
83
+ //#region src/errors/InvalidJwtError.ts
84
+ var InvalidJwtError = class extends BananalinkError {
85
+ constructor() {
86
+ super("Invalid Bananalink dapp jwt");
87
+ }
88
+ };
89
+
82
90
  //#endregion
83
91
  //#region src/jwt/createBananalinkJwks.ts
84
92
  function createBananalinkJwks(apiUrl) {
@@ -149,49 +157,195 @@ var InvalidConnectionStateError = class extends BananalinkError {
149
157
  }
150
158
  };
151
159
 
160
+ //#endregion
161
+ //#region src/errors/PendingSessionAbortedError.ts
162
+ var PendingSessionAbortedError = class extends BananalinkError {
163
+ constructor() {
164
+ super("Pending session was intentionally aborted");
165
+ this.name = "PendingSessionAbortedError";
166
+ }
167
+ };
168
+
169
+ //#endregion
170
+ //#region src/api/createDappMessage.ts
171
+ function isCreateDappMessageResponse(body) {
172
+ return typeof body === "object" && body !== null && "messageId" in body && typeof body.messageId === "string";
173
+ }
174
+ async function createDappMessage(apiUrl, accessToken, params) {
175
+ const response = await fetch(`${apiUrl}/dapps/messages`, {
176
+ method: "POST",
177
+ headers: {
178
+ Authorization: `Bearer ${accessToken}`,
179
+ "Content-Type": "application/json"
180
+ },
181
+ body: JSON.stringify(params)
182
+ });
183
+ if (response.ok) {
184
+ const body = await response.json().catch(() => {
185
+ throw new DappApiError("Invalid JSON response from bananalink API", response.status);
186
+ });
187
+ if (!isCreateDappMessageResponse(body)) throw new DappApiError("Unexpected response shape from bananalink API", response.status);
188
+ return body;
189
+ }
190
+ throw new DappApiError("Failed to create dapp message", response.status);
191
+ }
192
+
193
+ //#endregion
194
+ //#region src/errors/RequestRejectedError.ts
195
+ var RequestRejectedError = class extends BananalinkError {
196
+ constructor(messageId) {
197
+ super(`Request rejected by wallet (messageId: ${messageId})`);
198
+ this.messageId = messageId;
199
+ this.name = "RequestRejectedError";
200
+ }
201
+ };
202
+
203
+ //#endregion
204
+ //#region src/errors/RequestTimeoutError.ts
205
+ var RequestTimeoutError = class extends BananalinkError {
206
+ constructor(method) {
207
+ super(`Timeout reached for request ${method}`);
208
+ this.method = method;
209
+ }
210
+ };
211
+
212
+ //#endregion
213
+ //#region src/errors/SessionClosedError.ts
214
+ var SessionClosedError = class extends BananalinkError {
215
+ constructor() {
216
+ super("Session is closed");
217
+ }
218
+ };
219
+
152
220
  //#endregion
153
221
  //#region src/core/BananalinkSession.ts
154
- var BananalinkSession = class {
155
- constructor(sessionClaims, ws) {
222
+ var BananalinkSession = class BananalinkSession {
223
+ static REQUEST_TIMEOUT_MS = 600 * 1e3;
224
+ pendingRequests = /* @__PURE__ */ new Map();
225
+ stopListening;
226
+ stopListeningOnClose;
227
+ closed;
228
+ static RESULT_HANDLERS = { eth_sendTransaction: (result) => {
229
+ if (typeof result === "object" && result !== null && "txHash" in result && typeof result.txHash === "string") return result.txHash;
230
+ throw new BananalinkError(`Unexpected eth_sendTransaction result: ${result}`);
231
+ } };
232
+ constructor(sessionClaims, ws, apiUrl) {
156
233
  this.sessionClaims = sessionClaims;
157
234
  this.ws = ws;
235
+ this.apiUrl = apiUrl;
236
+ this.stopListening = this.ws.onMessage((payload) => {
237
+ if ((0, _bananalink_test_sdk_core.isMessageResponseMessage)(payload)) this.handleMessageResponse(payload);
238
+ });
239
+ this.stopListeningOnClose = this.ws.onClose(() => {
240
+ this.cleanup();
241
+ this.closed = true;
242
+ });
243
+ this.closed = false;
244
+ }
245
+ async request({ method, params, timeoutMs = BananalinkSession.REQUEST_TIMEOUT_MS }) {
246
+ if (this.closed) throw new SessionClosedError();
247
+ const { messageId } = await createDappMessage(this.apiUrl, this.sessionClaims.accessToken, {
248
+ method,
249
+ payload: params
250
+ });
251
+ if (this.closed) throw new SessionClosedError();
252
+ return new Promise((resolve, reject) => {
253
+ let timeout;
254
+ if (timeoutMs > 0) timeout = setTimeout(() => {
255
+ this.pendingRequests.delete(messageId);
256
+ reject(new RequestTimeoutError(method));
257
+ }, timeoutMs);
258
+ this.pendingRequests.set(messageId, {
259
+ timeout,
260
+ handle: (messageResponse) => {
261
+ if (messageResponse.status === "rejected") {
262
+ reject(new RequestRejectedError(messageId));
263
+ return;
264
+ }
265
+ const resultHandler = BananalinkSession.RESULT_HANDLERS[method];
266
+ try {
267
+ resolve(resultHandler(messageResponse.payloadResponse));
268
+ } catch (error) {
269
+ reject(new BananalinkError(`Failed resolving request response, ${error}`));
270
+ }
271
+ },
272
+ reject
273
+ });
274
+ });
158
275
  }
159
276
  close() {
160
- this.ws.close();
277
+ this.cleanup();
278
+ if (!this.ws.closed) this.ws.close();
279
+ this.closed = true;
280
+ }
281
+ handleMessageResponse(messageResponseMessage) {
282
+ const pending = this.pendingRequests.get(messageResponseMessage.msgId);
283
+ if (!pending) return;
284
+ this.pendingRequests.delete(messageResponseMessage.msgId);
285
+ clearTimeout(pending.timeout);
286
+ pending.handle(messageResponseMessage);
287
+ }
288
+ cleanup() {
289
+ this.rejectAllPendingRequests();
290
+ this.stopListening();
291
+ this.stopListeningOnClose();
292
+ }
293
+ rejectAllPendingRequests() {
294
+ for (const [, pending] of this.pendingRequests) {
295
+ clearTimeout(pending.timeout);
296
+ pending.reject(/* @__PURE__ */ new Error("Pending requests rejected"));
297
+ }
298
+ this.pendingRequests.clear();
161
299
  }
162
300
  };
163
301
 
164
302
  //#endregion
165
303
  //#region src/core/BananalinkConnection.ts
166
304
  var BananalinkConnection = class BananalinkConnection {
167
- static AUTH_TIMEOUT_MILLIS = 60 * 1e3;
305
+ static AUTH_TIMEOUT_MILLIS = 600 * 1e3;
306
+ apiUrl;
168
307
  wsUrl;
169
308
  jwt;
170
309
  dappId;
171
310
  dappInstanceId;
172
311
  nonce;
312
+ aborting = false;
313
+ abortPendingSessionRequest = null;
314
+ pendingSessionPromise = null;
173
315
  constructor(opts) {
316
+ this.apiUrl = opts.apiUrl;
174
317
  this.wsUrl = opts.wsUrl;
175
318
  this.jwt = opts.jwt;
176
319
  this.dappId = opts.dappId;
177
320
  this.dappInstanceId = opts.dappInstanceId;
178
321
  this.nonce = opts.nonce;
322
+ if (!this.dappInstanceId && !this.jwt) throw new InvalidConnectionStateError("Cannot get session without authorizing a dapp instance or providing a valid JWT");
179
323
  }
180
324
  async getSession() {
181
- if (this.jwt) throw new BananalinkError("Recovering a lost dapp connection is not yet implemented");
182
- const dappInstanceId = this.dappInstanceId;
183
- if (!dappInstanceId) throw new InvalidConnectionStateError("Cannot get session without authorizing the dapp instance or providing a valid JWT");
184
- let ws;
185
- try {
186
- ws = await (0, _bananalink_test_sdk_core.connectWebSocket)({
187
- url: `${this.wsUrl}?dappInstanceId=${encodeURIComponent(dappInstanceId)}`,
188
- pingIntervalMs: 3e4,
189
- pongTimeoutMs: 5e3
190
- });
191
- } catch (error) {
192
- throw new BananalinkError("Unable to establish dapp websocket connection", { cause: error });
325
+ if (this.pendingSessionPromise) return this.pendingSessionPromise;
326
+ if (this.jwt) {
327
+ const { jwtPayload, rawJwt } = this.jwt;
328
+ const ws = await this.getTransportHandle();
329
+ if (this.aborting) {
330
+ ws.close();
331
+ this.aborting = false;
332
+ throw new PendingSessionAbortedError();
333
+ }
334
+ BananalinkConnection.bind(ws, rawJwt);
335
+ return new BananalinkSession({
336
+ address: jwtPayload.address,
337
+ message: jwtPayload.message,
338
+ signature: jwtPayload.signature,
339
+ accessToken: rawJwt
340
+ }, ws, this.apiUrl);
193
341
  }
194
- return await new Promise((resolve, reject) => {
342
+ const ws = await this.getTransportHandle(this.dappInstanceId);
343
+ if (this.aborting) {
344
+ ws.close();
345
+ this.aborting = false;
346
+ throw new PendingSessionAbortedError();
347
+ }
348
+ this.pendingSessionPromise = new Promise((resolve, reject) => {
195
349
  let settled = false;
196
350
  const authTimeout = setTimeout(() => {
197
351
  settleReject(new ConnectionTimeoutError());
@@ -204,7 +358,7 @@ var BananalinkConnection = class BananalinkConnection {
204
358
  message: payload.message,
205
359
  signature: payload.signature,
206
360
  accessToken: payload.accessToken
207
- }, ws));
361
+ }, ws, this.apiUrl));
208
362
  return;
209
363
  }
210
364
  if ((0, _bananalink_test_sdk_core.isRejectedMessage)(payload)) settleReject(new ConnectionRejectedError());
@@ -212,11 +366,17 @@ var BananalinkConnection = class BananalinkConnection {
212
366
  const stopListeningToClose = ws.onClose(() => {
213
367
  settleReject(new BananalinkError("Connection closed before authorization completed"));
214
368
  });
215
- function cleanup() {
369
+ this.abortPendingSessionRequest = () => {
370
+ settleReject(new PendingSessionAbortedError());
371
+ };
372
+ const cleanup = () => {
373
+ this.abortPendingSessionRequest = null;
216
374
  clearTimeout(authTimeout);
217
375
  stopListeningToMessages();
218
376
  stopListeningToClose();
219
- }
377
+ this.aborting = false;
378
+ this.pendingSessionPromise = null;
379
+ };
220
380
  function settleResolve(session) {
221
381
  if (settled) return;
222
382
  settled = true;
@@ -231,6 +391,24 @@ var BananalinkConnection = class BananalinkConnection {
231
391
  reject(error);
232
392
  }
233
393
  });
394
+ return await this.pendingSessionPromise;
395
+ }
396
+ abortPendingSession() {
397
+ if (this.aborting) return;
398
+ this.aborting = true;
399
+ this.abortPendingSessionRequest?.();
400
+ }
401
+ async getTransportHandle(dappInstanceId) {
402
+ const url = `${this.wsUrl}${dappInstanceId != null ? `?dappInstanceId=${encodeURIComponent(dappInstanceId)}` : ""}`;
403
+ try {
404
+ return await (0, _bananalink_test_sdk_core.connectWebSocket)({
405
+ url,
406
+ pingIntervalMs: 3e4,
407
+ pongTimeoutMs: 5e3
408
+ });
409
+ } catch (error) {
410
+ throw new BananalinkError("Unable to establish dapp websocket connection", { cause: error });
411
+ }
234
412
  }
235
413
  get connectionUrl() {
236
414
  if (!this.dappInstanceId || !this.nonce) throw new InvalidConnectionStateError("Cannot get connection URL without a dapp instance");
@@ -238,9 +416,6 @@ var BananalinkConnection = class BananalinkConnection {
238
416
  const nonce = this.nonce;
239
417
  return `bananalink://?dappId=${encodeURIComponent(this.dappId)}&dappInstanceId=${encodeURIComponent(dappInstanceId)}&nonce=${encodeURIComponent(nonce)}`;
240
418
  }
241
- get authorized() {
242
- return this.jwt !== void 0;
243
- }
244
419
  static bind(ws, jwt) {
245
420
  const bindMessage = {
246
421
  type: "bind",
@@ -269,17 +444,19 @@ var BananalinkClient = class BananalinkClient {
269
444
  }
270
445
  async connect(opts) {
271
446
  const rawJwt = opts?.jwt;
272
- const jwtPayload = await (rawJwt ? this.getJwtPayload(rawJwt) : Promise.resolve(null));
273
- if (!jwtPayload || !rawJwt) {
447
+ if (!rawJwt) {
274
448
  const { dappInstanceId, nonce } = await this.connectDappInstance();
275
449
  return new BananalinkConnection({
276
450
  jwt: void 0,
277
451
  dappId: this.dapp.dappId,
278
452
  dappInstanceId,
279
453
  nonce,
454
+ apiUrl: this.apiUrl,
280
455
  wsUrl: this.wsUrl
281
456
  });
282
457
  }
458
+ const jwtPayload = await this.getJwtPayload(rawJwt);
459
+ if (!jwtPayload) throw new InvalidJwtError();
283
460
  return new BananalinkConnection({
284
461
  jwt: {
285
462
  jwtPayload,
@@ -288,6 +465,7 @@ var BananalinkClient = class BananalinkClient {
288
465
  dappId: this.dapp.dappId,
289
466
  dappInstanceId: null,
290
467
  nonce: null,
468
+ apiUrl: this.apiUrl,
291
469
  wsUrl: this.wsUrl
292
470
  });
293
471
  }
@@ -325,4 +503,9 @@ exports.ConnectionTimeoutError = ConnectionTimeoutError;
325
503
  exports.DappApiError = DappApiError;
326
504
  exports.InvalidConfigError = InvalidConfigError;
327
505
  exports.InvalidConnectionStateError = InvalidConnectionStateError;
506
+ exports.InvalidJwtError = InvalidJwtError;
507
+ exports.PendingSessionAbortedError = PendingSessionAbortedError;
508
+ exports.RequestRejectedError = RequestRejectedError;
509
+ exports.RequestTimeoutError = RequestTimeoutError;
510
+ exports.SessionClosedError = SessionClosedError;
328
511
  exports.displayBananalinkQR = displayBananalinkQR;
package/dist/index.d.cts CHANGED
@@ -9,6 +9,31 @@ type JwtPayload = {
9
9
  jwt: string;
10
10
  };
11
11
  //#endregion
12
+ //#region src/types/TransactionRequest.d.ts
13
+ type TransactionRequest = {
14
+ to: string;
15
+ value?: string;
16
+ data?: string;
17
+ chainId?: number;
18
+ };
19
+ //#endregion
20
+ //#region src/types/RequestMap.d.ts
21
+ type RequestMap = {
22
+ eth_sendTransaction: {
23
+ params: [TransactionRequest];
24
+ result: string;
25
+ };
26
+ };
27
+ //#endregion
28
+ //#region src/types/Request.d.ts
29
+ type Request = keyof RequestMap;
30
+ //#endregion
31
+ //#region src/types/RequestParams.d.ts
32
+ type RequestParams<T extends keyof RequestMap> = RequestMap[T]['params'];
33
+ //#endregion
34
+ //#region src/types/RequestResult.d.ts
35
+ type RequestResult<T extends keyof RequestMap> = RequestMap[T]['result'];
36
+ //#endregion
12
37
  //#region src/types/SessionClaims.d.ts
13
38
  type SessionClaims = {
14
39
  address: string;
@@ -21,8 +46,27 @@ type SessionClaims = {
21
46
  declare class BananalinkSession {
22
47
  readonly sessionClaims: SessionClaims;
23
48
  private readonly ws;
24
- constructor(sessionClaims: SessionClaims, ws: TransportHandle);
49
+ private readonly apiUrl;
50
+ private static readonly REQUEST_TIMEOUT_MS;
51
+ private readonly pendingRequests;
52
+ private readonly stopListening;
53
+ private readonly stopListeningOnClose;
54
+ private closed;
55
+ private static readonly RESULT_HANDLERS;
56
+ constructor(sessionClaims: SessionClaims, ws: TransportHandle, apiUrl: string);
57
+ request<T extends Request>({
58
+ method,
59
+ params,
60
+ timeoutMs
61
+ }: {
62
+ method: T;
63
+ params?: RequestParams<T>;
64
+ timeoutMs?: number;
65
+ }): Promise<RequestResult<T>>;
25
66
  close(): void;
67
+ private handleMessageResponse;
68
+ private cleanup;
69
+ private rejectAllPendingRequests;
26
70
  }
27
71
  //#endregion
28
72
  //#region src/core/BananalinkConnection.d.ts
@@ -32,12 +76,17 @@ type BananalinkDappJwt = {
32
76
  } | undefined;
33
77
  declare class BananalinkConnection {
34
78
  private static readonly AUTH_TIMEOUT_MILLIS;
79
+ private readonly apiUrl;
35
80
  private readonly wsUrl;
36
81
  private readonly jwt;
37
82
  private readonly dappId;
38
83
  private readonly dappInstanceId;
39
84
  private readonly nonce;
85
+ private aborting;
86
+ private abortPendingSessionRequest;
87
+ private pendingSessionPromise;
40
88
  constructor(opts: {
89
+ apiUrl: string;
41
90
  wsUrl: string;
42
91
  jwt: BananalinkDappJwt;
43
92
  dappId: string;
@@ -45,8 +94,9 @@ declare class BananalinkConnection {
45
94
  nonce: string | null;
46
95
  });
47
96
  getSession(): Promise<BananalinkSession>;
97
+ abortPendingSession(): void;
98
+ private getTransportHandle;
48
99
  get connectionUrl(): string;
49
- get authorized(): boolean;
50
100
  private static bind;
51
101
  }
52
102
  //#endregion
@@ -101,8 +151,35 @@ declare class InvalidConnectionStateError extends BananalinkError {
101
151
  constructor(message: string);
102
152
  }
103
153
  //#endregion
154
+ //#region src/errors/InvalidJwtError.d.ts
155
+ declare class InvalidJwtError extends BananalinkError {
156
+ constructor();
157
+ }
158
+ //#endregion
159
+ //#region src/errors/PendingSessionAbortedError.d.ts
160
+ declare class PendingSessionAbortedError extends BananalinkError {
161
+ constructor();
162
+ }
163
+ //#endregion
164
+ //#region src/errors/RequestRejectedError.d.ts
165
+ declare class RequestRejectedError extends BananalinkError {
166
+ readonly messageId: string;
167
+ constructor(messageId: string);
168
+ }
169
+ //#endregion
170
+ //#region src/errors/RequestTimeoutError.d.ts
171
+ declare class RequestTimeoutError extends BananalinkError {
172
+ readonly method: Request;
173
+ constructor(method: Request);
174
+ }
175
+ //#endregion
176
+ //#region src/errors/SessionClosedError.d.ts
177
+ declare class SessionClosedError extends BananalinkError {
178
+ constructor();
179
+ }
180
+ //#endregion
104
181
  //#region src/utils/displayBananalinkQR.d.ts
105
182
  declare function displayBananalinkQR(url: string): Promise<string>;
106
183
  //#endregion
107
- export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, type Dapp, DappApiError, InvalidConfigError, InvalidConnectionStateError, displayBananalinkQR };
184
+ export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, type Dapp, DappApiError, InvalidConfigError, InvalidConnectionStateError, InvalidJwtError, PendingSessionAbortedError, type Request, type RequestMap, type RequestParams, RequestRejectedError, type RequestResult, RequestTimeoutError, SessionClosedError, type TransactionRequest, displayBananalinkQR };
108
185
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types/JwtPayload.ts","../src/types/SessionClaims.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,UAAA;EAAe,OAAA;EAAiB,MAAA;EAAgB,OAAA;EAAiB,SAAA;EAAmB,GAAA;AAAA;;;KCApF,aAAA;EAAkB,OAAA;EAAiB,OAAA;EAAiB,SAAA;EAAmB,WAAA;AAAA;;;cCGtE,iBAAA;EAAA,SAEO,aAAA,EAAe,aAAA;EAAA,iBACd,EAAA;cADD,aAAA,EAAe,aAAA,EACd,EAAA,EAAI,eAAA;EAGhB,KAAA,CAAA;AAAA;;;KCKJ,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,IAqFxB,aAAA,CAAA;EAAA,IASA,UAAA,CAAA;EAAA,eAKI,IAAA;AAAA;;;cChIJ,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,MAAA;EAAA;EAcK,OAAA,CAAQ,IAAA;IAAS,GAAA;EAAA,IAAgB,OAAA,CAAQ,oBAAA;EAAA,QAsBxC,aAAA;EAAA,QAIA,mBAAA;AAAA;;;cC5DH,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"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types/JwtPayload.ts","../src/types/TransactionRequest.ts","../src/types/RequestMap.ts","../src/types/Request.ts","../src/types/RequestParams.ts","../src/types/RequestResult.ts","../src/types/SessionClaims.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/errors/InvalidJwtError.ts","../src/errors/PendingSessionAbortedError.ts","../src/errors/RequestRejectedError.ts","../src/errors/RequestTimeoutError.ts","../src/errors/SessionClosedError.ts","../src/utils/displayBananalinkQR.ts"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -9,6 +9,31 @@ type JwtPayload = {
9
9
  jwt: string;
10
10
  };
11
11
  //#endregion
12
+ //#region src/types/TransactionRequest.d.ts
13
+ type TransactionRequest = {
14
+ to: string;
15
+ value?: string;
16
+ data?: string;
17
+ chainId?: number;
18
+ };
19
+ //#endregion
20
+ //#region src/types/RequestMap.d.ts
21
+ type RequestMap = {
22
+ eth_sendTransaction: {
23
+ params: [TransactionRequest];
24
+ result: string;
25
+ };
26
+ };
27
+ //#endregion
28
+ //#region src/types/Request.d.ts
29
+ type Request = keyof RequestMap;
30
+ //#endregion
31
+ //#region src/types/RequestParams.d.ts
32
+ type RequestParams<T extends keyof RequestMap> = RequestMap[T]['params'];
33
+ //#endregion
34
+ //#region src/types/RequestResult.d.ts
35
+ type RequestResult<T extends keyof RequestMap> = RequestMap[T]['result'];
36
+ //#endregion
12
37
  //#region src/types/SessionClaims.d.ts
13
38
  type SessionClaims = {
14
39
  address: string;
@@ -21,8 +46,27 @@ type SessionClaims = {
21
46
  declare class BananalinkSession {
22
47
  readonly sessionClaims: SessionClaims;
23
48
  private readonly ws;
24
- constructor(sessionClaims: SessionClaims, ws: TransportHandle);
49
+ private readonly apiUrl;
50
+ private static readonly REQUEST_TIMEOUT_MS;
51
+ private readonly pendingRequests;
52
+ private readonly stopListening;
53
+ private readonly stopListeningOnClose;
54
+ private closed;
55
+ private static readonly RESULT_HANDLERS;
56
+ constructor(sessionClaims: SessionClaims, ws: TransportHandle, apiUrl: string);
57
+ request<T extends Request>({
58
+ method,
59
+ params,
60
+ timeoutMs
61
+ }: {
62
+ method: T;
63
+ params?: RequestParams<T>;
64
+ timeoutMs?: number;
65
+ }): Promise<RequestResult<T>>;
25
66
  close(): void;
67
+ private handleMessageResponse;
68
+ private cleanup;
69
+ private rejectAllPendingRequests;
26
70
  }
27
71
  //#endregion
28
72
  //#region src/core/BananalinkConnection.d.ts
@@ -32,12 +76,17 @@ type BananalinkDappJwt = {
32
76
  } | undefined;
33
77
  declare class BananalinkConnection {
34
78
  private static readonly AUTH_TIMEOUT_MILLIS;
79
+ private readonly apiUrl;
35
80
  private readonly wsUrl;
36
81
  private readonly jwt;
37
82
  private readonly dappId;
38
83
  private readonly dappInstanceId;
39
84
  private readonly nonce;
85
+ private aborting;
86
+ private abortPendingSessionRequest;
87
+ private pendingSessionPromise;
40
88
  constructor(opts: {
89
+ apiUrl: string;
41
90
  wsUrl: string;
42
91
  jwt: BananalinkDappJwt;
43
92
  dappId: string;
@@ -45,8 +94,9 @@ declare class BananalinkConnection {
45
94
  nonce: string | null;
46
95
  });
47
96
  getSession(): Promise<BananalinkSession>;
97
+ abortPendingSession(): void;
98
+ private getTransportHandle;
48
99
  get connectionUrl(): string;
49
- get authorized(): boolean;
50
100
  private static bind;
51
101
  }
52
102
  //#endregion
@@ -101,8 +151,35 @@ declare class InvalidConnectionStateError extends BananalinkError {
101
151
  constructor(message: string);
102
152
  }
103
153
  //#endregion
154
+ //#region src/errors/InvalidJwtError.d.ts
155
+ declare class InvalidJwtError extends BananalinkError {
156
+ constructor();
157
+ }
158
+ //#endregion
159
+ //#region src/errors/PendingSessionAbortedError.d.ts
160
+ declare class PendingSessionAbortedError extends BananalinkError {
161
+ constructor();
162
+ }
163
+ //#endregion
164
+ //#region src/errors/RequestRejectedError.d.ts
165
+ declare class RequestRejectedError extends BananalinkError {
166
+ readonly messageId: string;
167
+ constructor(messageId: string);
168
+ }
169
+ //#endregion
170
+ //#region src/errors/RequestTimeoutError.d.ts
171
+ declare class RequestTimeoutError extends BananalinkError {
172
+ readonly method: Request;
173
+ constructor(method: Request);
174
+ }
175
+ //#endregion
176
+ //#region src/errors/SessionClosedError.d.ts
177
+ declare class SessionClosedError extends BananalinkError {
178
+ constructor();
179
+ }
180
+ //#endregion
104
181
  //#region src/utils/displayBananalinkQR.d.ts
105
182
  declare function displayBananalinkQR(url: string): Promise<string>;
106
183
  //#endregion
107
- export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, type Dapp, DappApiError, InvalidConfigError, InvalidConnectionStateError, displayBananalinkQR };
184
+ export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, type Dapp, DappApiError, InvalidConfigError, InvalidConnectionStateError, InvalidJwtError, PendingSessionAbortedError, type Request, type RequestMap, type RequestParams, RequestRejectedError, type RequestResult, RequestTimeoutError, SessionClosedError, type TransactionRequest, displayBananalinkQR };
108
185
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/JwtPayload.ts","../src/types/SessionClaims.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,UAAA;EAAe,OAAA;EAAiB,MAAA;EAAgB,OAAA;EAAiB,SAAA;EAAmB,GAAA;AAAA;;;KCApF,aAAA;EAAkB,OAAA;EAAiB,OAAA;EAAiB,SAAA;EAAmB,WAAA;AAAA;;;cCGtE,iBAAA;EAAA,SAEO,aAAA,EAAe,aAAA;EAAA,iBACd,EAAA;cADD,aAAA,EAAe,aAAA,EACd,EAAA,EAAI,eAAA;EAGhB,KAAA,CAAA;AAAA;;;KCKJ,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,IAqFxB,aAAA,CAAA;EAAA,IASA,UAAA,CAAA;EAAA,eAKI,IAAA;AAAA;;;cChIJ,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,MAAA;EAAA;EAcK,OAAA,CAAQ,IAAA;IAAS,GAAA;EAAA,IAAgB,OAAA,CAAQ,oBAAA;EAAA,QAsBxC,aAAA;EAAA,QAIA,mBAAA;AAAA;;;cC5DH,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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/JwtPayload.ts","../src/types/TransactionRequest.ts","../src/types/RequestMap.ts","../src/types/Request.ts","../src/types/RequestParams.ts","../src/types/RequestResult.ts","../src/types/SessionClaims.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/errors/InvalidJwtError.ts","../src/errors/PendingSessionAbortedError.ts","../src/errors/RequestRejectedError.ts","../src/errors/RequestTimeoutError.ts","../src/errors/SessionClosedError.ts","../src/utils/displayBananalinkQR.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRemoteJWKSet, jwtVerify } from "jose";
2
- import { connectWebSocket, isAuthorizedMessage, isRejectedMessage } from "@bananalink-test/sdk-core";
2
+ import { connectWebSocket, isAuthorizedMessage, isMessageResponseMessage, isRejectedMessage } from "@bananalink-test/sdk-core";
3
3
  import QRCode from "qrcode";
4
4
 
5
5
  //#region src/errors/BananalinkError.ts
@@ -50,6 +50,14 @@ var InvalidConfigError = class extends BananalinkError {
50
50
  }
51
51
  };
52
52
 
53
+ //#endregion
54
+ //#region src/errors/InvalidJwtError.ts
55
+ var InvalidJwtError = class extends BananalinkError {
56
+ constructor() {
57
+ super("Invalid Bananalink dapp jwt");
58
+ }
59
+ };
60
+
53
61
  //#endregion
54
62
  //#region src/jwt/createBananalinkJwks.ts
55
63
  function createBananalinkJwks(apiUrl) {
@@ -120,49 +128,195 @@ var InvalidConnectionStateError = class extends BananalinkError {
120
128
  }
121
129
  };
122
130
 
131
+ //#endregion
132
+ //#region src/errors/PendingSessionAbortedError.ts
133
+ var PendingSessionAbortedError = class extends BananalinkError {
134
+ constructor() {
135
+ super("Pending session was intentionally aborted");
136
+ this.name = "PendingSessionAbortedError";
137
+ }
138
+ };
139
+
140
+ //#endregion
141
+ //#region src/api/createDappMessage.ts
142
+ function isCreateDappMessageResponse(body) {
143
+ return typeof body === "object" && body !== null && "messageId" in body && typeof body.messageId === "string";
144
+ }
145
+ async function createDappMessage(apiUrl, accessToken, params) {
146
+ const response = await fetch(`${apiUrl}/dapps/messages`, {
147
+ method: "POST",
148
+ headers: {
149
+ Authorization: `Bearer ${accessToken}`,
150
+ "Content-Type": "application/json"
151
+ },
152
+ body: JSON.stringify(params)
153
+ });
154
+ if (response.ok) {
155
+ const body = await response.json().catch(() => {
156
+ throw new DappApiError("Invalid JSON response from bananalink API", response.status);
157
+ });
158
+ if (!isCreateDappMessageResponse(body)) throw new DappApiError("Unexpected response shape from bananalink API", response.status);
159
+ return body;
160
+ }
161
+ throw new DappApiError("Failed to create dapp message", response.status);
162
+ }
163
+
164
+ //#endregion
165
+ //#region src/errors/RequestRejectedError.ts
166
+ var RequestRejectedError = class extends BananalinkError {
167
+ constructor(messageId) {
168
+ super(`Request rejected by wallet (messageId: ${messageId})`);
169
+ this.messageId = messageId;
170
+ this.name = "RequestRejectedError";
171
+ }
172
+ };
173
+
174
+ //#endregion
175
+ //#region src/errors/RequestTimeoutError.ts
176
+ var RequestTimeoutError = class extends BananalinkError {
177
+ constructor(method) {
178
+ super(`Timeout reached for request ${method}`);
179
+ this.method = method;
180
+ }
181
+ };
182
+
183
+ //#endregion
184
+ //#region src/errors/SessionClosedError.ts
185
+ var SessionClosedError = class extends BananalinkError {
186
+ constructor() {
187
+ super("Session is closed");
188
+ }
189
+ };
190
+
123
191
  //#endregion
124
192
  //#region src/core/BananalinkSession.ts
125
- var BananalinkSession = class {
126
- constructor(sessionClaims, ws) {
193
+ var BananalinkSession = class BananalinkSession {
194
+ static REQUEST_TIMEOUT_MS = 600 * 1e3;
195
+ pendingRequests = /* @__PURE__ */ new Map();
196
+ stopListening;
197
+ stopListeningOnClose;
198
+ closed;
199
+ static RESULT_HANDLERS = { eth_sendTransaction: (result) => {
200
+ if (typeof result === "object" && result !== null && "txHash" in result && typeof result.txHash === "string") return result.txHash;
201
+ throw new BananalinkError(`Unexpected eth_sendTransaction result: ${result}`);
202
+ } };
203
+ constructor(sessionClaims, ws, apiUrl) {
127
204
  this.sessionClaims = sessionClaims;
128
205
  this.ws = ws;
206
+ this.apiUrl = apiUrl;
207
+ this.stopListening = this.ws.onMessage((payload) => {
208
+ if (isMessageResponseMessage(payload)) this.handleMessageResponse(payload);
209
+ });
210
+ this.stopListeningOnClose = this.ws.onClose(() => {
211
+ this.cleanup();
212
+ this.closed = true;
213
+ });
214
+ this.closed = false;
215
+ }
216
+ async request({ method, params, timeoutMs = BananalinkSession.REQUEST_TIMEOUT_MS }) {
217
+ if (this.closed) throw new SessionClosedError();
218
+ const { messageId } = await createDappMessage(this.apiUrl, this.sessionClaims.accessToken, {
219
+ method,
220
+ payload: params
221
+ });
222
+ if (this.closed) throw new SessionClosedError();
223
+ return new Promise((resolve, reject) => {
224
+ let timeout;
225
+ if (timeoutMs > 0) timeout = setTimeout(() => {
226
+ this.pendingRequests.delete(messageId);
227
+ reject(new RequestTimeoutError(method));
228
+ }, timeoutMs);
229
+ this.pendingRequests.set(messageId, {
230
+ timeout,
231
+ handle: (messageResponse) => {
232
+ if (messageResponse.status === "rejected") {
233
+ reject(new RequestRejectedError(messageId));
234
+ return;
235
+ }
236
+ const resultHandler = BananalinkSession.RESULT_HANDLERS[method];
237
+ try {
238
+ resolve(resultHandler(messageResponse.payloadResponse));
239
+ } catch (error) {
240
+ reject(new BananalinkError(`Failed resolving request response, ${error}`));
241
+ }
242
+ },
243
+ reject
244
+ });
245
+ });
129
246
  }
130
247
  close() {
131
- this.ws.close();
248
+ this.cleanup();
249
+ if (!this.ws.closed) this.ws.close();
250
+ this.closed = true;
251
+ }
252
+ handleMessageResponse(messageResponseMessage) {
253
+ const pending = this.pendingRequests.get(messageResponseMessage.msgId);
254
+ if (!pending) return;
255
+ this.pendingRequests.delete(messageResponseMessage.msgId);
256
+ clearTimeout(pending.timeout);
257
+ pending.handle(messageResponseMessage);
258
+ }
259
+ cleanup() {
260
+ this.rejectAllPendingRequests();
261
+ this.stopListening();
262
+ this.stopListeningOnClose();
263
+ }
264
+ rejectAllPendingRequests() {
265
+ for (const [, pending] of this.pendingRequests) {
266
+ clearTimeout(pending.timeout);
267
+ pending.reject(/* @__PURE__ */ new Error("Pending requests rejected"));
268
+ }
269
+ this.pendingRequests.clear();
132
270
  }
133
271
  };
134
272
 
135
273
  //#endregion
136
274
  //#region src/core/BananalinkConnection.ts
137
275
  var BananalinkConnection = class BananalinkConnection {
138
- static AUTH_TIMEOUT_MILLIS = 60 * 1e3;
276
+ static AUTH_TIMEOUT_MILLIS = 600 * 1e3;
277
+ apiUrl;
139
278
  wsUrl;
140
279
  jwt;
141
280
  dappId;
142
281
  dappInstanceId;
143
282
  nonce;
283
+ aborting = false;
284
+ abortPendingSessionRequest = null;
285
+ pendingSessionPromise = null;
144
286
  constructor(opts) {
287
+ this.apiUrl = opts.apiUrl;
145
288
  this.wsUrl = opts.wsUrl;
146
289
  this.jwt = opts.jwt;
147
290
  this.dappId = opts.dappId;
148
291
  this.dappInstanceId = opts.dappInstanceId;
149
292
  this.nonce = opts.nonce;
293
+ if (!this.dappInstanceId && !this.jwt) throw new InvalidConnectionStateError("Cannot get session without authorizing a dapp instance or providing a valid JWT");
150
294
  }
151
295
  async getSession() {
152
- if (this.jwt) throw new BananalinkError("Recovering a lost dapp connection is not yet implemented");
153
- const dappInstanceId = this.dappInstanceId;
154
- if (!dappInstanceId) throw new InvalidConnectionStateError("Cannot get session without authorizing the dapp instance or providing a valid JWT");
155
- let ws;
156
- try {
157
- ws = await connectWebSocket({
158
- url: `${this.wsUrl}?dappInstanceId=${encodeURIComponent(dappInstanceId)}`,
159
- pingIntervalMs: 3e4,
160
- pongTimeoutMs: 5e3
161
- });
162
- } catch (error) {
163
- throw new BananalinkError("Unable to establish dapp websocket connection", { cause: error });
296
+ if (this.pendingSessionPromise) return this.pendingSessionPromise;
297
+ if (this.jwt) {
298
+ const { jwtPayload, rawJwt } = this.jwt;
299
+ const ws = await this.getTransportHandle();
300
+ if (this.aborting) {
301
+ ws.close();
302
+ this.aborting = false;
303
+ throw new PendingSessionAbortedError();
304
+ }
305
+ BananalinkConnection.bind(ws, rawJwt);
306
+ return new BananalinkSession({
307
+ address: jwtPayload.address,
308
+ message: jwtPayload.message,
309
+ signature: jwtPayload.signature,
310
+ accessToken: rawJwt
311
+ }, ws, this.apiUrl);
164
312
  }
165
- return await new Promise((resolve, reject) => {
313
+ const ws = await this.getTransportHandle(this.dappInstanceId);
314
+ if (this.aborting) {
315
+ ws.close();
316
+ this.aborting = false;
317
+ throw new PendingSessionAbortedError();
318
+ }
319
+ this.pendingSessionPromise = new Promise((resolve, reject) => {
166
320
  let settled = false;
167
321
  const authTimeout = setTimeout(() => {
168
322
  settleReject(new ConnectionTimeoutError());
@@ -175,7 +329,7 @@ var BananalinkConnection = class BananalinkConnection {
175
329
  message: payload.message,
176
330
  signature: payload.signature,
177
331
  accessToken: payload.accessToken
178
- }, ws));
332
+ }, ws, this.apiUrl));
179
333
  return;
180
334
  }
181
335
  if (isRejectedMessage(payload)) settleReject(new ConnectionRejectedError());
@@ -183,11 +337,17 @@ var BananalinkConnection = class BananalinkConnection {
183
337
  const stopListeningToClose = ws.onClose(() => {
184
338
  settleReject(new BananalinkError("Connection closed before authorization completed"));
185
339
  });
186
- function cleanup() {
340
+ this.abortPendingSessionRequest = () => {
341
+ settleReject(new PendingSessionAbortedError());
342
+ };
343
+ const cleanup = () => {
344
+ this.abortPendingSessionRequest = null;
187
345
  clearTimeout(authTimeout);
188
346
  stopListeningToMessages();
189
347
  stopListeningToClose();
190
- }
348
+ this.aborting = false;
349
+ this.pendingSessionPromise = null;
350
+ };
191
351
  function settleResolve(session) {
192
352
  if (settled) return;
193
353
  settled = true;
@@ -202,6 +362,24 @@ var BananalinkConnection = class BananalinkConnection {
202
362
  reject(error);
203
363
  }
204
364
  });
365
+ return await this.pendingSessionPromise;
366
+ }
367
+ abortPendingSession() {
368
+ if (this.aborting) return;
369
+ this.aborting = true;
370
+ this.abortPendingSessionRequest?.();
371
+ }
372
+ async getTransportHandle(dappInstanceId) {
373
+ const url = `${this.wsUrl}${dappInstanceId != null ? `?dappInstanceId=${encodeURIComponent(dappInstanceId)}` : ""}`;
374
+ try {
375
+ return await connectWebSocket({
376
+ url,
377
+ pingIntervalMs: 3e4,
378
+ pongTimeoutMs: 5e3
379
+ });
380
+ } catch (error) {
381
+ throw new BananalinkError("Unable to establish dapp websocket connection", { cause: error });
382
+ }
205
383
  }
206
384
  get connectionUrl() {
207
385
  if (!this.dappInstanceId || !this.nonce) throw new InvalidConnectionStateError("Cannot get connection URL without a dapp instance");
@@ -209,9 +387,6 @@ var BananalinkConnection = class BananalinkConnection {
209
387
  const nonce = this.nonce;
210
388
  return `bananalink://?dappId=${encodeURIComponent(this.dappId)}&dappInstanceId=${encodeURIComponent(dappInstanceId)}&nonce=${encodeURIComponent(nonce)}`;
211
389
  }
212
- get authorized() {
213
- return this.jwt !== void 0;
214
- }
215
390
  static bind(ws, jwt) {
216
391
  const bindMessage = {
217
392
  type: "bind",
@@ -240,17 +415,19 @@ var BananalinkClient = class BananalinkClient {
240
415
  }
241
416
  async connect(opts) {
242
417
  const rawJwt = opts?.jwt;
243
- const jwtPayload = await (rawJwt ? this.getJwtPayload(rawJwt) : Promise.resolve(null));
244
- if (!jwtPayload || !rawJwt) {
418
+ if (!rawJwt) {
245
419
  const { dappInstanceId, nonce } = await this.connectDappInstance();
246
420
  return new BananalinkConnection({
247
421
  jwt: void 0,
248
422
  dappId: this.dapp.dappId,
249
423
  dappInstanceId,
250
424
  nonce,
425
+ apiUrl: this.apiUrl,
251
426
  wsUrl: this.wsUrl
252
427
  });
253
428
  }
429
+ const jwtPayload = await this.getJwtPayload(rawJwt);
430
+ if (!jwtPayload) throw new InvalidJwtError();
254
431
  return new BananalinkConnection({
255
432
  jwt: {
256
433
  jwtPayload,
@@ -259,6 +436,7 @@ var BananalinkClient = class BananalinkClient {
259
436
  dappId: this.dapp.dappId,
260
437
  dappInstanceId: null,
261
438
  nonce: null,
439
+ apiUrl: this.apiUrl,
262
440
  wsUrl: this.wsUrl
263
441
  });
264
442
  }
@@ -287,5 +465,5 @@ async function displayBananalinkQR(url) {
287
465
  }
288
466
 
289
467
  //#endregion
290
- export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, DappApiError, InvalidConfigError, InvalidConnectionStateError, displayBananalinkQR };
468
+ export { BananalinkClient, BananalinkConnection, BananalinkError, BananalinkSession, ConnectionRejectedError, ConnectionTimeoutError, DappApiError, InvalidConfigError, InvalidConnectionStateError, InvalidJwtError, PendingSessionAbortedError, RequestRejectedError, RequestTimeoutError, SessionClosedError, displayBananalinkQR };
291
469
  //# sourceMappingURL=index.mjs.map
@@ -1 +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 type { CreateDappInstanceResponse, Dapp } from '@bananalink-test/sdk-core';\nimport { DappApiError } from '../errors/DappApiError.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<{ message: string; signature: string }>(jwt, jwks, {\n algorithms: ['ES256'],\n issuer: 'bananalink',\n requiredClaims: ['sub', 'aud', 'message', 'signature'],\n });\n return {\n address: payload.sub as string,\n dappId: Array.isArray(payload.aud) ? payload.aud[0] : (payload.aud as string),\n message: payload.message,\n signature: payload.signature,\n jwt,\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","import type { TransportHandle } from '@bananalink-test/sdk-core';\nimport type { SessionClaims } from '../types/SessionClaims.js';\n\nexport class BananalinkSession {\n constructor(\n public readonly sessionClaims: SessionClaims,\n private readonly ws: TransportHandle,\n ) {}\n\n public close(): void {\n this.ws.close();\n }\n}\n","import {\n type BindMessage,\n connectWebSocket,\n isAuthorizedMessage,\n isRejectedMessage,\n type TransportHandle,\n} from '@bananalink-test/sdk-core';\nimport { 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 let ws: TransportHandle;\n try {\n ws = await connectWebSocket({\n url: `${this.wsUrl}?dappInstanceId=${encodeURIComponent(dappInstanceId)}`,\n pingIntervalMs: 30_000,\n pongTimeoutMs: 5_000,\n });\n } catch (error) {\n throw new BananalinkError('Unable to establish dapp websocket connection', { cause: error });\n }\n\n return await new Promise((resolve, reject) => {\n let settled = false;\n\n const authTimeout = setTimeout(() => {\n settleReject(new ConnectionTimeoutError());\n }, BananalinkConnection.AUTH_TIMEOUT_MILLIS);\n\n const stopListeningToMessages = ws.onMessage((payload: unknown) => {\n if (isAuthorizedMessage(payload)) {\n BananalinkConnection.bind(ws, payload.accessToken);\n settleResolve(\n new BananalinkSession(\n {\n address: payload.address,\n message: payload.message,\n signature: payload.signature,\n accessToken: payload.accessToken,\n },\n ws,\n ),\n );\n return;\n }\n\n if (isRejectedMessage(payload)) {\n settleReject(new ConnectionRejectedError());\n }\n });\n\n const stopListeningToClose = ws.onClose(() => {\n settleReject(new BananalinkError('Connection closed before authorization completed'));\n });\n\n function cleanup(): void {\n clearTimeout(authTimeout);\n stopListeningToMessages();\n stopListeningToClose();\n }\n\n function settleResolve(session: BananalinkSession): void {\n if (settled) {\n return;\n }\n\n settled = true;\n cleanup();\n resolve(session);\n }\n\n function settleReject(error: Error): void {\n if (settled) {\n return;\n }\n\n settled = true;\n cleanup();\n ws.close();\n reject(error);\n }\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: TransportHandle, jwt: string): void {\n const bindMessage: BindMessage = { type: 'bind', jwt };\n ws.send(bindMessage);\n }\n}\n","import type { CreateDappInstanceResponse, Dapp } from '@bananalink-test/sdk-core';\nimport { 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 { 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":"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"}
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/errors/InvalidJwtError.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/errors/PendingSessionAbortedError.ts","../src/api/createDappMessage.ts","../src/errors/RequestRejectedError.ts","../src/errors/RequestTimeoutError.ts","../src/errors/SessionClosedError.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 type { CreateDappInstanceResponse, Dapp } from '@bananalink-test/sdk-core';\nimport { DappApiError } from '../errors/DappApiError.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 { BananalinkError } from './BananalinkError.js';\n\nexport class InvalidJwtError extends BananalinkError {\n constructor() {\n super('Invalid Bananalink dapp jwt');\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<{ message: string; signature: string }>(jwt, jwks, {\n algorithms: ['ES256'],\n issuer: 'bananalink',\n requiredClaims: ['sub', 'aud', 'message', 'signature'],\n });\n return {\n address: payload.sub as string,\n dappId: Array.isArray(payload.aud) ? payload.aud[0] : (payload.aud as string),\n message: payload.message,\n signature: payload.signature,\n jwt,\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","import { BananalinkError } from './BananalinkError.js';\n\nexport class PendingSessionAbortedError extends BananalinkError {\n constructor() {\n super('Pending session was intentionally aborted');\n this.name = 'PendingSessionAbortedError';\n }\n}\n","import { DappApiError } from '../errors/DappApiError.js';\n\nfunction isCreateDappMessageResponse(body: unknown): body is { messageId: string } {\n return typeof body === 'object' && body !== null && 'messageId' in body && typeof body.messageId === 'string';\n}\n\nexport async function createDappMessage(\n apiUrl: string,\n accessToken: string,\n params: {\n method: string;\n payload: unknown;\n },\n): Promise<{ messageId: string }> {\n const response = await fetch(`${apiUrl}/dapps/messages`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\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 (!isCreateDappMessageResponse(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 message', response.status);\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class RequestRejectedError extends BananalinkError {\n constructor(public readonly messageId: string) {\n super(`Request rejected by wallet (messageId: ${messageId})`);\n this.name = 'RequestRejectedError';\n }\n}\n","import type { Request } from '../types/Request.js';\nimport { BananalinkError } from './BananalinkError.js';\n\nexport class RequestTimeoutError extends BananalinkError {\n constructor(public readonly method: Request) {\n super(`Timeout reached for request ${method}`);\n }\n}\n","import { BananalinkError } from './BananalinkError.js';\n\nexport class SessionClosedError extends BananalinkError {\n constructor() {\n super('Session is closed');\n }\n}\n","import { isMessageResponseMessage, type MessageResponseMessage, type TransportHandle } from '@bananalink-test/sdk-core';\nimport { createDappMessage } from '../api/createDappMessage.js';\nimport { BananalinkError } from '../errors/BananalinkError.js';\nimport { RequestRejectedError } from '../errors/RequestRejectedError.js';\nimport { RequestTimeoutError } from '../errors/RequestTimeoutError.js';\nimport { SessionClosedError } from '../errors/SessionClosedError.js';\nimport type { PendingMessageRequest } from '../types/PendingMessageRequest.js';\nimport type { Request } from '../types/Request.js';\nimport type { RequestParams } from '../types/RequestParams.js';\nimport type { RequestResult } from '../types/RequestResult.js';\nimport type { RequestResultHandler } from '../types/RequestResultHandler.js';\nimport type { SessionClaims } from '../types/SessionClaims.js';\n\nexport class BananalinkSession {\n private static readonly REQUEST_TIMEOUT_MS = 10 * 60 * 1000;\n private readonly pendingRequests = new Map<string, PendingMessageRequest>();\n private readonly stopListening: () => void;\n private readonly stopListeningOnClose: () => void;\n private closed: boolean;\n\n private static readonly RESULT_HANDLERS: { [T in Request]: RequestResultHandler<T> } = {\n eth_sendTransaction: (result) => {\n if (typeof result === 'object' && result !== null && 'txHash' in result && typeof result.txHash === 'string') {\n return result.txHash;\n }\n throw new BananalinkError(`Unexpected eth_sendTransaction result: ${result}`);\n },\n };\n\n constructor(\n public readonly sessionClaims: SessionClaims,\n private readonly ws: TransportHandle,\n private readonly apiUrl: string,\n ) {\n this.stopListening = this.ws.onMessage((payload) => {\n if (isMessageResponseMessage(payload)) {\n this.handleMessageResponse(payload);\n }\n });\n this.stopListeningOnClose = this.ws.onClose(() => {\n this.cleanup();\n this.closed = true;\n });\n this.closed = false;\n }\n\n public async request<T extends Request>({\n method,\n params,\n timeoutMs = BananalinkSession.REQUEST_TIMEOUT_MS,\n }: {\n method: T;\n params?: RequestParams<T>;\n timeoutMs?: number;\n }): Promise<RequestResult<T>> {\n if (this.closed) {\n throw new SessionClosedError();\n }\n const { messageId } = await createDappMessage(this.apiUrl, this.sessionClaims.accessToken, {\n method,\n payload: params,\n });\n\n if (this.closed) {\n throw new SessionClosedError();\n }\n\n return new Promise<RequestResult<T>>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n if (timeoutMs > 0) {\n timeout = setTimeout(() => {\n this.pendingRequests.delete(messageId);\n reject(new RequestTimeoutError(method));\n }, timeoutMs);\n }\n\n this.pendingRequests.set(messageId, {\n timeout,\n handle: (messageResponse: MessageResponseMessage) => {\n if (messageResponse.status === 'rejected') {\n reject(new RequestRejectedError(messageId));\n return;\n }\n const resultHandler = BananalinkSession.RESULT_HANDLERS[method];\n try {\n resolve(resultHandler(messageResponse.payloadResponse));\n } catch (error: unknown) {\n reject(new BananalinkError(`Failed resolving request response, ${error}`));\n }\n },\n reject,\n });\n });\n }\n\n public close(): void {\n this.cleanup();\n if (!this.ws.closed) {\n this.ws.close();\n }\n this.closed = true;\n }\n\n private handleMessageResponse(messageResponseMessage: MessageResponseMessage): void {\n const pending = this.pendingRequests.get(messageResponseMessage.msgId);\n if (!pending) return;\n\n this.pendingRequests.delete(messageResponseMessage.msgId);\n clearTimeout(pending.timeout);\n pending.handle(messageResponseMessage);\n }\n\n private cleanup(): void {\n this.rejectAllPendingRequests();\n this.stopListening();\n this.stopListeningOnClose();\n }\n\n private rejectAllPendingRequests(): void {\n for (const [, pending] of this.pendingRequests) {\n clearTimeout(pending.timeout);\n // TODO: propagate error or throw generic one\n pending.reject(new Error('Pending requests rejected'));\n }\n this.pendingRequests.clear();\n }\n}\n","import {\n type BindMessage,\n connectWebSocket,\n isAuthorizedMessage,\n isRejectedMessage,\n type TransportHandle,\n} from '@bananalink-test/sdk-core';\nimport { BananalinkError } from '../errors/BananalinkError.js';\nimport { ConnectionRejectedError } from '../errors/ConnectionRejectedError.js';\nimport { ConnectionTimeoutError } from '../errors/ConnectionTimeoutError.js';\nimport { InvalidConnectionStateError } from '../errors/InvalidConnectionStateError.js';\nimport { PendingSessionAbortedError } from '../errors/PendingSessionAbortedError.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 = 10 * 60 * 1000;\n private readonly apiUrl: string;\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 private aborting: boolean = false;\n private abortPendingSessionRequest: (() => void) | null = null;\n private pendingSessionPromise: Promise<BananalinkSession> | null = null;\n\n constructor(opts: {\n apiUrl: string;\n wsUrl: string;\n jwt: BananalinkDappJwt;\n dappId: string;\n dappInstanceId: string | null;\n nonce: string | null;\n }) {\n this.apiUrl = opts.apiUrl;\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 if (!this.dappInstanceId && !this.jwt) {\n throw new InvalidConnectionStateError(\n 'Cannot get session without authorizing a dapp instance or providing a valid JWT',\n );\n }\n }\n\n public async getSession(): Promise<BananalinkSession> {\n if (this.pendingSessionPromise) {\n return this.pendingSessionPromise;\n }\n if (this.jwt) {\n const { jwtPayload, rawJwt } = this.jwt;\n const ws = await this.getTransportHandle();\n if (this.aborting) {\n ws.close();\n this.aborting = false;\n throw new PendingSessionAbortedError();\n }\n BananalinkConnection.bind(ws, rawJwt);\n return new BananalinkSession(\n {\n address: jwtPayload.address,\n message: jwtPayload.message,\n signature: jwtPayload.signature,\n accessToken: rawJwt,\n },\n ws,\n this.apiUrl,\n );\n }\n\n const ws = await this.getTransportHandle(this.dappInstanceId);\n if (this.aborting) {\n ws.close();\n this.aborting = false;\n throw new PendingSessionAbortedError();\n }\n\n this.pendingSessionPromise = new Promise((resolve, reject) => {\n let settled = false;\n\n const authTimeout = setTimeout(() => {\n settleReject(new ConnectionTimeoutError());\n }, BananalinkConnection.AUTH_TIMEOUT_MILLIS);\n\n const stopListeningToMessages = ws.onMessage((payload: unknown) => {\n if (isAuthorizedMessage(payload)) {\n BananalinkConnection.bind(ws, payload.accessToken);\n settleResolve(\n new BananalinkSession(\n {\n address: payload.address,\n message: payload.message,\n signature: payload.signature,\n accessToken: payload.accessToken,\n },\n ws,\n this.apiUrl,\n ),\n );\n return;\n }\n\n if (isRejectedMessage(payload)) {\n settleReject(new ConnectionRejectedError());\n }\n });\n\n const stopListeningToClose = ws.onClose(() => {\n settleReject(new BananalinkError('Connection closed before authorization completed'));\n });\n\n this.abortPendingSessionRequest = () => {\n settleReject(new PendingSessionAbortedError());\n };\n\n const cleanup = (): void => {\n this.abortPendingSessionRequest = null;\n clearTimeout(authTimeout);\n stopListeningToMessages();\n stopListeningToClose();\n this.aborting = false;\n this.pendingSessionPromise = null;\n };\n\n function settleResolve(session: BananalinkSession): void {\n if (settled) {\n return;\n }\n\n settled = true;\n cleanup();\n resolve(session);\n }\n\n function settleReject(error: Error): void {\n if (settled) {\n return;\n }\n\n settled = true;\n cleanup();\n ws.close();\n reject(error);\n }\n });\n return await this.pendingSessionPromise;\n }\n\n public abortPendingSession(): void {\n if (this.aborting) {\n return;\n }\n this.aborting = true;\n this.abortPendingSessionRequest?.();\n }\n\n private async getTransportHandle(dappInstanceId?: string | null): Promise<TransportHandle> {\n const url = `${this.wsUrl}${dappInstanceId != null ? `?dappInstanceId=${encodeURIComponent(dappInstanceId)}` : ''}`;\n try {\n return await connectWebSocket({\n url,\n pingIntervalMs: 30_000,\n pongTimeoutMs: 5_000,\n });\n } catch (error) {\n throw new BananalinkError('Unable to establish dapp websocket connection', { cause: error });\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 private static bind(ws: TransportHandle, jwt: string): void {\n const bindMessage: BindMessage = { type: 'bind', jwt };\n ws.send(bindMessage);\n }\n}\n","import type { CreateDappInstanceResponse, Dapp } from '@bananalink-test/sdk-core';\nimport { createDappInstance } from '../api/createDappInstance.js';\nimport { InvalidConfigError } from '../errors/InvalidConfigError.js';\nimport { InvalidJwtError } from '../errors/InvalidJwtError.js';\nimport { createBananalinkJwks } from '../jwt/createBananalinkJwks.js';\nimport { verifyJwt } from '../jwt/verifyJwt.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 if (!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 apiUrl: this.apiUrl,\n wsUrl: this.wsUrl,\n });\n }\n\n const jwtPayload = await this.getJwtPayload(rawJwt);\n if (!jwtPayload) {\n throw new InvalidJwtError();\n }\n\n return new BananalinkConnection({\n jwt: { jwtPayload, rawJwt },\n dappId: this.dapp.dappId,\n dappInstanceId: null,\n nonce: null,\n apiUrl: this.apiUrl,\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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananalink-test/client",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "jose": "^6.1.3",
36
36
  "qrcode": "^1.5.4",
37
- "@bananalink-test/sdk-core": "0.2.0"
37
+ "@bananalink-test/sdk-core": "0.3.1"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsdown",