@notabene/javascript-sdk 2.19.0 → 2.19.1

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/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "author": "Notabene <developers@notabene.id>",
11
11
  "license": "MIT",
12
12
  "packageManager": "yarn@4.5.1",
13
- "version": "2.19.0",
13
+ "version": "2.19.1",
14
14
  "source": "src/notabene.ts",
15
15
  "main": "dist/cjs/notabene.cjs",
16
16
  "module": "dist/esm/notabene.js",
@@ -0,0 +1,66 @@
1
+ import { beforeEach, describe, expect, test } from 'vitest';
2
+ import EmbeddedComponent from '../components/EmbeddedComponent';
3
+
4
+ /**
5
+ * Integration test (no mocks) reproducing the Banxa crash (PAY-1056):
6
+ *
7
+ * TypeError: Cannot set properties of undefined (setting 'onmessage')
8
+ *
9
+ * Cause: Reown/WalletConnect calls `parent.postMessage(event, '*')` from
10
+ * inside the widget iframe without transferring a MessagePort. The SDK's
11
+ * message listener passed `event.ports[0]` (undefined) to `setPort()`.
12
+ */
13
+
14
+ interface TestValue {
15
+ id?: string;
16
+ }
17
+
18
+ describe('EmbeddedComponent — portless postMessage (PAY-1056)', () => {
19
+ let container: HTMLDivElement;
20
+
21
+ beforeEach(() => {
22
+ container = document.createElement('div');
23
+ document.body.appendChild(container);
24
+ });
25
+
26
+ test('should not crash when iframe sends a message without ports', () => {
27
+ const component = new EmbeddedComponent<TestValue, unknown>(
28
+ 'about:blank?widget=true',
29
+ {},
30
+ );
31
+ component.embed(container);
32
+
33
+ const iframe = container.querySelector('iframe')!;
34
+
35
+ expect(() => {
36
+ window.dispatchEvent(
37
+ new MessageEvent('message', {
38
+ source: iframe.contentWindow,
39
+ data: { type: 'wc_sessionUpdate' },
40
+ ports: [],
41
+ }),
42
+ );
43
+ }).not.toThrow();
44
+ });
45
+
46
+ test('should still accept the handshake message with a port', () => {
47
+ const component = new EmbeddedComponent<TestValue, unknown>(
48
+ 'about:blank?widget=true',
49
+ {},
50
+ );
51
+ component.embed(container);
52
+
53
+ const iframe = container.querySelector('iframe')!;
54
+ const channel = new MessageChannel();
55
+
56
+ expect(() => {
57
+ window.dispatchEvent(
58
+ new MessageEvent('message', {
59
+ source: iframe.contentWindow,
60
+ data: {},
61
+ ports: [channel.port2],
62
+ }),
63
+ );
64
+ }).not.toThrow();
65
+ });
66
+ });
@@ -12,6 +12,9 @@ interface TestOptions {
12
12
  currency?: string;
13
13
  }
14
14
 
15
+ // Store callbacks for testing
16
+ const callbacks = new Map();
17
+
15
18
  // Mock MessageEventManager
16
19
  vi.mock('../utils/MessageEventManager', () => {
17
20
  return {
@@ -30,9 +33,6 @@ vi.mock('../utils/MessageEventManager', () => {
30
33
  };
31
34
  });
32
35
 
33
- // Store callbacks for testing
34
- const callbacks = new Map();
35
-
36
36
  describe('EmbeddedComponent', () => {
37
37
  let mockParent: HTMLElement;
38
38
  let mockAppendChild: ReturnType<typeof vi.fn>;
@@ -24,6 +24,7 @@ export default class EmbeddedComponent<V, O> {
24
24
  private iframe?: HTMLIFrameElement;
25
25
  private eventManager: MessageEventManager<V, O>;
26
26
  private modal?: HTMLDialogElement;
27
+ private messageHandler?: (event: MessageEvent) => void;
27
28
 
28
29
  /**
29
30
  * Creates an instance of EmbeddedComponent.
@@ -112,18 +113,24 @@ export default class EmbeddedComponent<V, O> {
112
113
  // this.iframe.style.backgroundColor = 'transparent';
113
114
  parent.appendChild(this.iframe);
114
115
 
115
- window.addEventListener('message', (event: MessageEvent) => {
116
+ this.messageHandler = (event: MessageEvent) => {
116
117
  if (event.source !== this.iframe?.contentWindow) {
117
118
  return;
118
119
  }
119
- // console.log('received message from iframe', event.data);
120
- this.eventManager?.setPort(event.ports[0]);
121
- });
120
+ if (event.ports[0]) {
121
+ this.eventManager?.setPort(event.ports[0]);
122
+ }
123
+ };
124
+ window.addEventListener('message', this.messageHandler);
122
125
 
123
126
  this.iframe?.contentWindow?.focus();
124
127
  }
125
128
 
126
129
  removeEmbed() {
130
+ if (this.messageHandler) {
131
+ window.removeEventListener('message', this.messageHandler);
132
+ this.messageHandler = undefined;
133
+ }
127
134
  if (this.iframe) this.iframe.remove();
128
135
  }
129
136
 
@@ -258,13 +265,15 @@ export default class EmbeddedComponent<V, O> {
258
265
  '_blank',
259
266
  'popup=true,width=600,height=600',
260
267
  );
261
- window.addEventListener('message', (event: MessageEvent) => {
268
+ const popupMessageHandler = (event: MessageEvent) => {
262
269
  if (event.source !== popup) {
263
270
  return;
264
271
  }
265
- console.log('received message from popup', event.data);
266
- this.eventManager?.setPort(event.ports[0]);
267
- });
272
+ if (event.ports[0]) {
273
+ this.eventManager?.setPort(event.ports[0]);
274
+ }
275
+ };
276
+ window.addEventListener('message', popupMessageHandler);
268
277
 
269
278
  const removeCancel = this.on(CMType.CANCEL, () => {
270
279
  popup?.close();
@@ -275,6 +284,7 @@ export default class EmbeddedComponent<V, O> {
275
284
  });
276
285
 
277
286
  return this.completion().finally(() => {
287
+ window.removeEventListener('message', popupMessageHandler);
278
288
  removeCancel();
279
289
  removeComplete();
280
290
  });
package/src/notabene.ts CHANGED
@@ -93,11 +93,9 @@ export {
93
93
  componentResponseToIVMS101,
94
94
  componentResponseToTxCreateRequest,
95
95
  componentResponseToTxRequests,
96
- componentResponseToV1TxCreateRequest,
97
96
  } from './responseTransformer';
98
97
  export type {
99
98
  ResponseToTxRequestConfig,
100
- TransactionCreateRequest,
101
99
  TransactionCreateRequestV2,
102
100
  TransactionIVMS101Request,
103
101
  } from './responseTransformer';
@@ -58,26 +58,6 @@ const ivms101Body = componentResponseToIVMS101(response, delegateToken, {
58
58
  });
59
59
  ```
60
60
 
61
- ### Version 1 API (Legacy)
62
-
63
- ```typescript
64
- import { componentResponseToV1TxCreateRequest } from '$lib/notabene-tx-transformer';
65
-
66
- withdrawal.on('complete', async (result) => {
67
- const requestBody = componentResponseToV1TxCreateRequest(result.response);
68
-
69
- // Send to your Version 1 API
70
- await fetch(`${apiUrlV1}/entity/${did}/tx`, {
71
- method: 'POST',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- Authorization: `Bearer ${token}`
75
- },
76
- body: JSON.stringify(requestBody)
77
- });
78
- });
79
- ```
80
-
81
61
  ## API
82
62
 
83
63
  ### `componentResponseToTxRequests(response, delegateToken, config?)`
@@ -127,16 +107,6 @@ Transforms a Notabene component response to IVMS101 format.
127
107
 
128
108
  **Returns:** IVMS101 formatted request body
129
109
 
130
- ### `componentResponseToV1TxCreateRequest(response)`
131
-
132
- Transforms a Notabene component response to a V1 transaction create request.
133
-
134
- **Parameters:**
135
-
136
- - `response`: Response from the Notabene TX Create component
137
-
138
- **Returns:** V1 transaction create request body
139
-
140
110
  ## Module Structure
141
111
 
142
112
  ```
@@ -333,7 +333,7 @@ describe('componentResponseToTxRequests', () => {
333
333
  geographicAddress: [geographicAddress],
334
334
  },
335
335
  legalPerson: undefined,
336
- accountNumber: [TEST_ORIGINATOR_ID],
336
+ accountNumber: [TEST_ADDRESS],
337
337
  },
338
338
  ],
339
339
  },
@@ -435,7 +435,7 @@ describe('componentResponseToTxRequests', () => {
435
435
  },
436
436
  },
437
437
  legalPerson: undefined,
438
- accountNumber: [TEST_BENEFICIARY_ID],
438
+ accountNumber: [TEST_ADDRESS],
439
439
  },
440
440
  ],
441
441
  },
@@ -9,13 +9,11 @@ export {
9
9
  componentResponseToIVMS101,
10
10
  componentResponseToTxCreateRequest,
11
11
  componentResponseToTxRequests,
12
- componentResponseToV1TxCreateRequest,
13
12
  } from './transformer';
14
13
 
15
14
  // Type exports
16
15
  export {
17
16
  type ResponseToTxRequestConfig,
18
- type TransactionCreateRequest,
19
- type TransactionCreateRequestV2, // rename to TransactCreateRequest?
17
+ type TransactionCreateRequestV2,
20
18
  type TransactionIVMS101Request,
21
19
  } from './types';
@@ -1,11 +1,4 @@
1
1
  import type { Agent, DID } from '@taprsvp/types';
2
- import type {
3
- Beneficiary,
4
- NaturalPerson,
5
- NaturalPersonName,
6
- Originator,
7
- Person,
8
- } from '../ivms';
9
2
  import {
10
3
  PersonType,
11
4
  type Deposit,
@@ -17,7 +10,6 @@ import type {
17
10
  BaseRequestConfig,
18
11
  ResponseToIVMS101RequestConfig,
19
12
  ResponseToTxCreateRequestConfig,
20
- TransactionCreateRequest,
21
13
  TransactionCreateRequestV2,
22
14
  TransactionIVMS101Request,
23
15
  } from './types';
@@ -28,212 +20,6 @@ import {
28
20
  isWithdrawal,
29
21
  } from './utils';
30
22
 
31
- // Constants
32
- const DEFAULT_GEOGRAPHIC_ADDRESS = [
33
- {
34
- addressType: 'GEOG' as const,
35
- addressLine: ['1234 Main Street'],
36
- townName: 'Unknown',
37
- country: 'US',
38
- },
39
- ];
40
-
41
- // Helper functions
42
- function parseName(fullName: string) {
43
- const trimmedName = fullName.trim();
44
- if (!trimmedName) {
45
- return { primaryIdentifier: '', secondaryIdentifier: '' };
46
- }
47
-
48
- const parts = trimmedName.split(/\s+/);
49
- const primaryIdentifier = parts[parts.length - 1];
50
- const secondaryParts = parts.slice(0, -1);
51
- const secondaryIdentifier =
52
- secondaryParts.length > 0 ? secondaryParts.join(' ') : primaryIdentifier;
53
-
54
- return { primaryIdentifier, secondaryIdentifier };
55
- }
56
-
57
- function copyIVMS(
58
- type: 'originator' | 'beneficiary',
59
- persons?: Person[],
60
- accountNumber: string[] = ['1234567890'],
61
- ): Originator | Beneficiary {
62
- const updatedPersons = persons?.map((person) => {
63
- const updatedPerson = { ...person };
64
-
65
- if (updatedPerson.naturalPerson) {
66
- updatedPerson.naturalPerson = {
67
- ...updatedPerson.naturalPerson,
68
- geographicAddress: DEFAULT_GEOGRAPHIC_ADDRESS,
69
- };
70
- }
71
-
72
- if (updatedPerson.legalPerson) {
73
- updatedPerson.legalPerson = {
74
- ...updatedPerson.legalPerson,
75
- geographicAddress: DEFAULT_GEOGRAPHIC_ADDRESS,
76
- };
77
- }
78
-
79
- return updatedPerson;
80
- });
81
-
82
- return type === 'originator'
83
- ? { originatorPersons: updatedPersons, accountNumber }
84
- : { beneficiaryPersons: updatedPersons, accountNumber };
85
- }
86
-
87
- function hostIVMS(
88
- type: 'originator' | 'beneficiary',
89
- name = 'John Doe',
90
- ): Originator | Beneficiary {
91
- const { primaryIdentifier, secondaryIdentifier } = parseName(name);
92
-
93
- const person: Person = {
94
- naturalPerson: {
95
- name: [
96
- {
97
- nameIdentifier: [
98
- {
99
- primaryIdentifier,
100
- ...(secondaryIdentifier ? { secondaryIdentifier } : {}),
101
- nameIdentifierType: 'LEGL',
102
- },
103
- ],
104
- },
105
- ] as any,
106
- geographicAddress: DEFAULT_GEOGRAPHIC_ADDRESS,
107
- },
108
- };
109
-
110
- return type === 'originator'
111
- ? { originatorPersons: [person], accountNumber: ['1234567890'] }
112
- : { beneficiaryPersons: [person], accountNumber: ['1234567890'] };
113
- }
114
-
115
- const wrapNameInArray = (naturalPerson: NaturalPerson): NaturalPerson => ({
116
- ...naturalPerson,
117
- // @ts-expect-error Preserving the original logic
118
- name: naturalPerson.name
119
- ? ([naturalPerson.name] as NaturalPersonName[])
120
- : undefined,
121
- });
122
-
123
- const transformPerson = (person: Person): Person => ({
124
- ...person,
125
- naturalPerson: person.naturalPerson
126
- ? wrapNameInArray(person.naturalPerson)
127
- : undefined,
128
- });
129
-
130
- const transformIVMS101 = (
131
- { beneficiary, originator }: IVMS101,
132
- name?: string,
133
- originatorEqualsBeneficiary?: boolean,
134
- destination?: string,
135
- operation?: 'withdrawal' | 'deposit',
136
- ) => {
137
- if (operation === 'withdrawal') {
138
- const transformedBeneficiary = beneficiary && {
139
- ...beneficiary,
140
- ...(destination && { accountNumber: [destination] }),
141
- beneficiaryPersons: beneficiary.beneficiaryPersons?.map(transformPerson),
142
- };
143
-
144
- if (
145
- originatorEqualsBeneficiary &&
146
- transformedBeneficiary?.beneficiaryPersons
147
- ) {
148
- const originator = copyIVMS(
149
- 'originator',
150
- transformedBeneficiary?.beneficiaryPersons,
151
- ) as Originator;
152
-
153
- return {
154
- beneficiary: {
155
- accountNumber: originator.accountNumber,
156
- beneficiaryPersons: originator.originatorPersons,
157
- },
158
- originator,
159
- };
160
- }
161
-
162
- return {
163
- beneficiary: transformedBeneficiary,
164
- originator: hostIVMS('originator', name),
165
- };
166
- }
167
-
168
- if (operation === 'deposit') {
169
- const transformedOriginator = originator && {
170
- ...(destination && { accountNumber: [destination] }),
171
- originatorPersons: originator.originatorPersons?.map(transformPerson),
172
- };
173
-
174
- if (
175
- originatorEqualsBeneficiary &&
176
- transformedOriginator?.originatorPersons
177
- ) {
178
- const beneficiary = copyIVMS(
179
- 'beneficiary',
180
- transformedOriginator?.originatorPersons,
181
- ) as Beneficiary;
182
-
183
- return {
184
- originator: {
185
- accountNumber: beneficiary.accountNumber,
186
- originatorPersons: beneficiary.beneficiaryPersons,
187
- },
188
- beneficiary,
189
- };
190
- }
191
-
192
- return {
193
- originator: transformedOriginator,
194
- beneficiary: hostIVMS('beneficiary', name),
195
- };
196
- }
197
-
198
- throw new Error('Invalid operation type');
199
- };
200
-
201
- export function mapToV1CreateRequest(
202
- withdrawal: Withdrawal,
203
- payload: V1Transaction,
204
- ivms101: IVMS101,
205
- ): TransactionCreateRequest {
206
- const { beneficiary, originator } = transformIVMS101(
207
- ivms101,
208
- withdrawal.customer?.name,
209
- payload.originatorEqualsBeneficiary,
210
- withdrawal.destination,
211
- 'withdrawal',
212
- );
213
-
214
- return {
215
- transactionAsset: payload.transactionAsset,
216
- transactionAmount: payload.transactionAmount,
217
- beneficiaryDid: withdrawal.counterparty?.did,
218
- originatorVASPdid: payload.originatorVASPdid,
219
- ...(payload.beneficiaryVASPdid && !payload.beneficiaryProof
220
- ? { beneficiaryVASPdid: payload.beneficiaryVASPdid }
221
- : {}),
222
- transactionBlockchainInfo: {
223
- ...(originator?.accountNumber && { origin: originator.accountNumber[0] }),
224
- ...(withdrawal.destination && { destination: withdrawal.destination }),
225
- },
226
- ...(payload.beneficiaryProof && {
227
- beneficiaryProof: payload.beneficiaryProof,
228
- }),
229
- ...(beneficiary && { beneficiary }),
230
- ...(originator && { originator }),
231
- ...(payload.originatorEqualsBeneficiary && {
232
- originatorEqualsBeneficiary: payload.originatorEqualsBeneficiary,
233
- }),
234
- };
235
- }
236
-
237
23
  export function mapToTransactCreateRequest(
238
24
  transaction: Withdrawal | Deposit,
239
25
  payload: V1Transaction,
@@ -311,7 +97,9 @@ export function mapToAppendPiiRequest(
311
97
  config: ResponseToIVMS101RequestConfig & Required<BaseRequestConfig>,
312
98
  ): TransactionIVMS101Request {
313
99
  if (isWithdrawal(transaction)) {
314
- const beneficiaryId = config.beneficiaryId;
100
+ // For outgoing transfers, the beneficiary is the counterparty — use the
101
+ // destination blockchain address as their accountNumber.
102
+ const beneficiaryAccountNumber = transaction.destination;
315
103
 
316
104
  const beneficiaryPersons =
317
105
  // If counterparty type is SELF, reuse originator data for beneficiary
@@ -319,7 +107,7 @@ export function mapToAppendPiiRequest(
319
107
  ? config.originator.originatorPerson
320
108
  : // Convert all beneficiary persons from V1 to V2 format
321
109
  ivms101.beneficiary?.beneficiaryPersons?.map((person) =>
322
- convertPersonToV2(person, beneficiaryId),
110
+ convertPersonToV2(person, beneficiaryAccountNumber),
323
111
  ) || [];
324
112
 
325
113
  return {
@@ -332,14 +120,17 @@ export function mapToAppendPiiRequest(
332
120
  };
333
121
  }
334
122
 
335
- const originatorId = config.originatorId;
123
+ // For incoming transfers, the originator is the counterparty — use the
124
+ // source blockchain address as their accountNumber.
125
+ const source = (transaction as Deposit).source;
126
+ const originatorAccountNumber = Array.isArray(source) ? source[0] : source;
336
127
 
337
128
  const originatorPersons =
338
129
  transaction.counterparty?.type === PersonType.SELF && config.beneficiary
339
130
  ? config.beneficiary.beneficiaryPerson
340
131
  : // Convert all originator persons from V1 to V2 format
341
132
  ivms101.originator?.originatorPersons?.map((person) =>
342
- convertPersonToV2(person, originatorId),
133
+ convertPersonToV2(person, originatorAccountNumber),
343
134
  ) || [];
344
135
 
345
136
  return {
@@ -8,18 +8,13 @@ import {
8
8
  type TransactionResponse,
9
9
  type Withdrawal,
10
10
  } from '../types';
11
- import {
12
- mapToAppendPiiRequest,
13
- mapToTransactCreateRequest,
14
- mapToV1CreateRequest,
15
- } from './mappers';
11
+ import { mapToAppendPiiRequest, mapToTransactCreateRequest } from './mappers';
16
12
  import type {
17
13
  BaseRequestConfig,
18
14
  DelegateToken,
19
15
  ResponseToIVMS101RequestConfig,
20
16
  ResponseToTxCreateRequestConfig,
21
17
  ResponseToTxRequestConfig,
22
- TransactionCreateRequest,
23
18
  TransactionCreateRequestV2,
24
19
  TransactionIVMS101Request,
25
20
  } from './types';
@@ -147,57 +142,6 @@ export function enrichConfig(
147
142
  Required<BaseRequestConfig>;
148
143
  }
149
144
 
150
- /**
151
- * Transforms a Notabene component response to a Version 1 API request body
152
- *
153
- * @param response - The response from the Notabene TX Create component
154
- * @returns The transformed request body ready for the Version 1 API
155
- *
156
- * @example
157
- * ```typescript
158
- * import { componentResponseToV1TxCreateRequest } from '$lib/notabene-tx-transformer';
159
- *
160
- * withdrawal.on('complete', async (result) => {
161
- * const requestBody = componentResponseToV1TxCreateRequest(result.response);
162
- *
163
- * await fetch(endpointUrl, {
164
- * method: 'POST',
165
- * body: JSON.stringify(requestBody)
166
- * });
167
- * });
168
- * ```
169
- */
170
- export function componentResponseToV1TxCreateRequest(
171
- response: TransactionResponse<Withdrawal>,
172
- ): TransactionCreateRequest {
173
- if (!response.txCreate || !response.ivms101) {
174
- throw new Error(
175
- 'Invalid response: missing required txCreate or ivms101 data',
176
- );
177
- }
178
-
179
- const { value, ivms101, proof, txCreate } = response;
180
-
181
- // Build withdrawal object from response data for V1 API
182
- const withdrawal: Withdrawal = {
183
- destination: value?.destination || '',
184
- counterparty: value?.counterparty || {},
185
- agent: value?.agent,
186
- account: value?.account,
187
- proof,
188
- asset:
189
- value?.asset ||
190
- (typeof txCreate.transactionAsset === 'string'
191
- ? txCreate.transactionAsset
192
- : txCreate.transactionAsset?.caip19) ||
193
- '',
194
- amountDecimal: value?.amountDecimal ?? txCreate.transactionAmount ?? 0,
195
- customer: value?.customer,
196
- } as Withdrawal;
197
-
198
- return mapToV1CreateRequest(withdrawal, txCreate, ivms101 as any);
199
- }
200
-
201
145
  /**
202
146
  * Transforms a Notabene component response into txCreate, IVMS101, and confirmRelationship request bodies
203
147
  *
@@ -32,24 +32,6 @@ export interface ResponseToIVMS101RequestConfig extends BaseRequestConfig {
32
32
  export type ResponseToTxRequestConfig = ResponseToTxCreateRequestConfig &
33
33
  ResponseToIVMS101RequestConfig;
34
34
 
35
- export interface TransactionCreateRequest {
36
- transactionAsset: any;
37
- transactionAmount: string;
38
- beneficiaryDid?: string;
39
- originatorVASPdid: string;
40
- beneficiaryVASPdid?: string;
41
- beneficiaryVASPname?: string;
42
- beneficiaryVASPwebsite?: string;
43
- transactionBlockchainInfo: {
44
- origin?: string;
45
- destination?: string;
46
- };
47
- beneficiaryProof?: any;
48
- beneficiary?: any;
49
- originator?: any;
50
- originatorEqualsBeneficiary?: boolean;
51
- }
52
-
53
35
  export interface TransactionCreateRequestV2 {
54
36
  originator: {
55
37
  '@id': string;
@@ -38,6 +38,8 @@ export class MessageEventManager<T, O> {
38
38
  */
39
39
 
40
40
  setPort(port: MessagePort): void {
41
+ if (!port) return;
42
+ this.port?.close();
41
43
  this.port = port;
42
44
  this.port.onmessage = this.handleMessage;
43
45
  this.port.start();
@@ -24,6 +24,14 @@ describe('MessageEventManager', () => {
24
24
  });
25
25
 
26
26
  describe('setPort', () => {
27
+ it('should ignore undefined port', () => {
28
+ expect(() =>
29
+ messageEventManager.setPort(undefined as unknown as MessagePort),
30
+ ).not.toThrow();
31
+ });
32
+ });
33
+
34
+ describe('setPort with valid port', () => {
27
35
  beforeEach(() => {
28
36
  messageEventManager.setPort(mockPort);
29
37
  });