idosell 0.4.43 → 0.4.45

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/changelog.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.45] - 2025-06-07
9
+ ### Added
10
+ - Added handleRaw to pass raw data instead of request
11
+ - Added test to webhooks
12
+
13
+
8
14
  ## [0.4.43] - 2025-06-07
9
15
  ### Changed
10
16
  - Webhook now supports HMAC checking and groups of webhooks
package/dist/app.d.ts CHANGED
@@ -7,109 +7,109 @@ import type {
7
7
  } from "./responses.d.ts";
8
8
 
9
9
  export interface ExecutableDumpParams {
10
- url: string;
11
- method: string;
12
- params: Record<string,any>;
10
+ url: string;
11
+ method: string;
12
+ params: Record<string, any>;
13
13
  }
14
14
 
15
15
  export interface ExecutableOptions {
16
- log?: boolean | ((obj: ExecutableDumpParams) => void),
16
+ log?: boolean | ((obj: ExecutableDumpParams) => void),
17
17
  dump?: boolean | ((obj: ExecutableDumpParams) => void),
18
18
  logPage?: boolean | ((text: string) => void),
19
- skipCheck?: boolean
19
+ skipCheck?: boolean
20
20
  }
21
21
 
22
22
  export type DateLike = string | number | Date;
23
23
 
24
- export type JSObject = Record<string,any>;
24
+ export type JSObject = Record<string, any>;
25
25
 
26
26
  export type RequestProxyObject = {
27
- auth: {
28
- url: string,
29
- apiKey: string,
30
- version: number|string
31
- },
32
- params: Record<string, any>,
27
+ auth: {
28
+ url: string,
29
+ apiKey: string,
30
+ version: number | string
31
+ },
32
+ params: Record<string, any>,
33
33
  }
34
34
 
35
35
  export type RequirementType = {
36
- any: string[]|true
37
- } | string | ((arg: RequestProxyObject | Record<string,any>) => string|false);
36
+ any: string[] | true
37
+ } | string | ((arg: RequestProxyObject | Record<string, any>) => string | false);
38
38
 
39
39
  export type GatewayRequestProxyObject = {
40
- gate: {
41
- method: 'get'|'post'|'put'|'delete',
42
- node: string
43
- },
44
- appendable?: {
45
- index: number,
46
- arrayNode: string,
47
- except: string[]
48
- },
49
- custom?: Record<string,(..._: any) => false|Record<string,any>>,
50
- snakeCase?: boolean,
51
- paginationObject?: boolean,
52
- next?: boolean,
53
- rootparams?: string|boolean,
54
- arrays?: string[],
55
- req?: RequirementType[],
56
- n?: Record<string,number>
40
+ gate: {
41
+ method: 'get' | 'post' | 'put' | 'delete',
42
+ node: string
43
+ },
44
+ appendable?: {
45
+ index: number,
46
+ arrayNode: string,
47
+ except: string[]
48
+ },
49
+ custom?: Record<string, (..._: any) => false | Record<string, any>>,
50
+ snakeCase?: boolean,
51
+ paginationObject?: boolean,
52
+ next?: boolean,
53
+ rootparams?: string | boolean,
54
+ arrays?: string[],
55
+ req?: RequirementType[],
56
+ n?: Record<string, number>
57
57
  } & RequestProxyObject;
58
58
 
59
59
  export interface Gateway<R = JSObject, P = JSObject> {
60
- /**
61
- * Executes the query to designated API endpoint
62
- * @param options Use options: log - to console log params, url and method, logPage - to console log current page in a loop
63
- * @returns Idosell response
64
- */
65
- exec: (options?: ExecutableOptions) => Promise<R>,
66
-
67
- /**
68
- * @returns Object with currently mapped parameters
69
- */
70
- getParams: () => P,
71
-
72
- /**
73
- * @description Set object as params
74
- */
75
- setParams: (params: P) => this
76
-
77
- /**
78
- * @description Checks if minimal parameters are provided. If not, throws an error.
79
- */
80
- checkParams: () => void
60
+ /**
61
+ * Executes the query to designated API endpoint
62
+ * @param options Use options: log - to console log params, url and method, logPage - to console log current page in a loop
63
+ * @returns Idosell response
64
+ */
65
+ exec: (options?: ExecutableOptions) => Promise<R>,
66
+
67
+ /**
68
+ * @returns Object with currently mapped parameters
69
+ */
70
+ getParams: () => P,
71
+
72
+ /**
73
+ * @description Set object as params
74
+ */
75
+ setParams: (params: P) => this
76
+
77
+ /**
78
+ * @description Checks if minimal parameters are provided. If not, throws an error.
79
+ */
80
+ checkParams: () => void
81
81
  }
82
82
 
83
- export interface PagableGateway<T,R = JSObject, P = JSObject> extends Gateway<R, P> {
84
- /**
83
+ export interface PagableGateway<T, R = JSObject, P = JSObject> extends Gateway<R, P> {
84
+ /**
85
85
  * @returns number of items i.e. products, orders, documents, etc.
86
86
  */
87
- count: () => Promise<number>,
87
+ count: () => Promise<number>,
88
88
 
89
- /**
89
+ /**
90
90
  * Allows to change offset and number of records returned
91
91
  * @param pageNumber - The page number to navigate to.
92
92
  * @param pageSize - The size of page
93
93
  * @returns The updated instance for method chaining.
94
94
  */
95
- page: (pageNumber: number, pageSize?: number) => T
95
+ page: (pageNumber: number, pageSize?: number) => T
96
96
 
97
- /**
98
- * @returns If completed request has more pages
99
- */
100
- hasNext: () => boolean
97
+ /**
98
+ * @returns If completed request has more pages
99
+ */
100
+ hasNext: () => boolean
101
101
  }
102
102
 
103
- export interface AppendableGateway<T,R = JSObject, P = JSObject> extends Gateway<R, P> {
104
- /**
105
- * Start creating next item in list
106
- */
107
- append: () => T
103
+ export interface AppendableGateway<T, R = JSObject, P = JSObject> extends Gateway<R, P> {
104
+ /**
105
+ * Start creating next item in list
106
+ */
107
+ append: () => T
108
108
  }
109
109
 
110
110
  export interface IdosellErrorFaultStructure {
111
- faultCode: number;
112
- faultString: string;
111
+ faultCode: number;
112
+ faultString: string;
113
113
  }
114
114
 
115
115
  // ─── Final normalized shape ──────────────────────────────────────────────────
@@ -244,12 +244,20 @@ export type DispatchResult =
244
244
  | { matched: false; eventType: string }
245
245
  | { matched: false; eventType: null; reason: "validation_failed" };
246
246
 
247
+
248
+ export interface RawWebhookInput {
249
+ headers: Record<string, string>;
250
+ rawBody: string; // always string — pre-parse
251
+ body?: Record<string, unknown>; // optional — caller may pre-parse
252
+ }
253
+
247
254
  export declare class WebhookChain {
248
255
  validateHeaders(validator: HeaderValidator): this;
249
256
  validateSignature(hmacKey: string): this;
250
257
  on<E extends WebhookEventType>(eventType: E, handler: WebhookHandler<E>): this;
251
258
  on<O extends WebhookObjectType>(objectType: O, handler: WebhookObjectHandler<O>): this;
252
259
  handle(req: import("node:http").IncomingMessage | Request): Promise<DispatchResult>;
260
+ handleRaw(req: RawWebhookInput): Promise<DispatchResult>;
253
261
  }
254
262
 
255
263
  export type Webhooks = {
@@ -259,4 +267,4 @@ export type Webhooks = {
259
267
  on<O extends WebhookObjectType>(objectType: O, handler: WebhookObjectHandler<O>): WebhookChain;
260
268
  };
261
269
 
262
- export {};
270
+ export { };
package/dist/webhooks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable no-unused-vars */
2
- import { normalizeIaiRequest, WebhookValidationError } from "./webhooks.normalizer.js";
2
+ import { extractHeaders, normalizeIaiRequest, WebhookValidationError } from "./webhooks.normalizer.js";
3
3
  import { createHmac } from "node:crypto";
4
4
  // ─── Runtime constants ────────────────────────────────────────────────────────
5
5
  export const WEBHOOK_OBJECT_TYPE = {
@@ -106,6 +106,14 @@ export class WebhookChain {
106
106
  }
107
107
  async handle(req) {
108
108
  const { headers, body, rawBody } = await normalizeIaiRequest(req);
109
+ return this.dispatch(headers, body, rawBody);
110
+ }
111
+ async handleRaw({ headers: rawHeaders, rawBody, body }) {
112
+ const headers = extractHeaders(rawHeaders);
113
+ const parsedBody = body ?? JSON.parse(rawBody);
114
+ return this.dispatch(headers, parsedBody, rawBody);
115
+ }
116
+ async dispatch(headers, body, rawBody) {
109
117
  if (this.validator !== null) {
110
118
  let valid = false;
111
119
  try {
@@ -118,7 +126,6 @@ export class WebhookChain {
118
126
  return { matched: false, eventType: null, reason: "validation_failed" };
119
127
  }
120
128
  }
121
- // Validate signature if a key was provided
122
129
  if (this.hmacKey) {
123
130
  const computed = createHmac("sha256", this.hmacKey).update(rawBody).digest("hex");
124
131
  if (computed !== headers.signature) {
@@ -39,7 +39,7 @@ function parseAuthHeader(headers) {
39
39
  }
40
40
  return token;
41
41
  }
42
- function extractHeaders(raw) {
42
+ export function extractHeaders(raw) {
43
43
  return {
44
44
  token: parseAuthHeader(raw),
45
45
  apiVersion: parseIntHeader(raw, "x-iai-api-version"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idosell",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
4
4
  "description": "Idosell 3 REST connector",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/gateways.d.ts",
@@ -0,0 +1,158 @@
1
+ // vitest tests for the webhook dispatcher
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { createHmac } from 'node:crypto';
4
+ import { webhooks } from '../dist/webhooks';
5
+ import { normalizeIaiRequest } from '../dist/webhooks.normalizer';
6
+
7
+ const API_VERSION = '8';
8
+ const SIGNATURE_KEY = 'secret-hmac-key';
9
+ const TOKEN = 'authentication-bearer-token'
10
+
11
+ // Helper to build a Request with appropriate headers and body
12
+ function buildRequest({
13
+ body,
14
+ eventType = 'productUpdated',
15
+ objectType = 'product',
16
+ }: {
17
+ body: unknown;
18
+ eventType?: string;
19
+ objectType?: string;
20
+ }) {
21
+ const rawBody = JSON.stringify(body);
22
+ const signature = createHmac('sha256', SIGNATURE_KEY).update(rawBody).digest('hex');
23
+ const headers = new Headers({
24
+ 'Content-Type': 'application/json',
25
+ 'x-iai-api-version': API_VERSION,
26
+ 'x-iai-event-time': new Date().toISOString(),
27
+ 'x-iai-event-type': eventType,
28
+ 'x-iai-event-uid': 'uid-123',
29
+ 'x-iai-object-type': objectType,
30
+ 'x-iai-panel-id': '1',
31
+ 'x-iai-signature': signature,
32
+ 'x-iai-webhook-time': new Date().toISOString(),
33
+ 'authorization': `Bearer ${TOKEN}`,
34
+ });
35
+ return new Request('https://api.example.com/webhook', {
36
+ method: 'POST',
37
+ headers,
38
+ body: rawBody,
39
+ });
40
+ }
41
+
42
+ describe('WebhookChain dispatcher', () => {
43
+ let mockHandler: ReturnType<typeof vi.fn>;
44
+
45
+ beforeEach(() => {
46
+ mockHandler = vi.fn();
47
+ });
48
+
49
+ it('routes a matching event to the registered handler', async () => {
50
+ const requestBody = {
51
+ results: [{ productDisplayedCode: 'ABC123' }],
52
+ };
53
+ const req = buildRequest({ body: requestBody });
54
+
55
+ const chain = webhooks
56
+ .validateHeaders((h) => h.token === TOKEN)
57
+ .validateSignature(SIGNATURE_KEY)
58
+ .on('productUpdated', mockHandler);
59
+
60
+ const result = await chain.handle(req);
61
+
62
+ expect(result).toEqual({ matched: true, eventType: 'productUpdated' });
63
+ expect(mockHandler).toHaveBeenCalledTimes(1);
64
+ const ctx = mockHandler.mock.calls[0][0];
65
+ expect(ctx.body).toEqual(requestBody);
66
+ expect(ctx.headers.eventType).toBe('productUpdated');
67
+ expect(ctx.headers.objectType).toBe('product');
68
+ });
69
+
70
+ it('returns unmatched when no handler is registered for the event', async () => {
71
+ const requestBody = { foo: 'bar' };
72
+ const req = buildRequest({ body: requestBody, eventType: 'orderCreated', objectType: 'order' });
73
+
74
+ const chain = webhooks
75
+ .validateHeaders((h) => h.token === TOKEN)
76
+ .validateSignature(SIGNATURE_KEY);
77
+
78
+ const result = await chain.handle(req);
79
+ expect(result).toEqual({ matched: false, eventType: 'orderCreated' });
80
+ expect(mockHandler).not.toHaveBeenCalled();
81
+ });
82
+
83
+ it('fails validateHeaders when token mismatched', async () => {
84
+ const requestBody = { foo: 'bar' };
85
+ const req = buildRequest({ body: requestBody });
86
+ const chain = webhooks
87
+ .validateHeaders(() => false)
88
+ .validateSignature(SIGNATURE_KEY)
89
+ .on('productUpdated', mockHandler);
90
+ const result = await chain.handle(req);
91
+ expect(result).toEqual({ matched: false, eventType: null, reason: 'validation_failed' });
92
+ });
93
+
94
+ it('fails validateSignature with wrong key', async () => {
95
+ const requestBody = { foo: 'bar' };
96
+ const req = buildRequest({ body: requestBody });
97
+ const chain = webhooks
98
+ .validateHeaders((h) => h.token === TOKEN)
99
+ .validateSignature('wrong-key')
100
+ .on('productUpdated', mockHandler);
101
+ await expect(chain.handle(req)).rejects.toThrow('Invalid webhook signature');
102
+ });
103
+
104
+ it('registers handler for object type "order" and matches order events', async () => {
105
+ const requestBody = { foo: 'bar' };
106
+ const req = buildRequest({ body: requestBody, eventType: 'orderCreated', objectType: 'order' });
107
+ const chain = webhooks
108
+ .validateHeaders((h) => h.token === TOKEN)
109
+ .validateSignature(SIGNATURE_KEY)
110
+ .on('order', mockHandler);
111
+ await expect(chain.handle(req)).resolves.not.toThrow();
112
+ });
113
+
114
+ it('normalizes request and verifies header shape', async () => {
115
+ const requestBody = { foo: 'bar' };
116
+ const req = buildRequest({ body: requestBody });
117
+ const { headers } = await normalizeIaiRequest(req);
118
+ // Basic shape checks
119
+ expect(headers).toMatchObject({
120
+ token: TOKEN,
121
+ apiVersion: Number(API_VERSION),
122
+ eventType: 'productUpdated',
123
+ objectType: 'product',
124
+ panelId: 1,
125
+ signature: expect.any(String),
126
+ });
127
+ // Type checks for dates
128
+ expect(headers.eventTime).toBeInstanceOf(Date);
129
+ expect(headers.webhookTime).toBeInstanceOf(Date);
130
+ });
131
+
132
+ it('handleRaw processes raw webhook input correctly', async () => {
133
+ const requestBody = { foo: 'bar' };
134
+ const rawBody = JSON.stringify(requestBody);
135
+ const rawHeaders = {
136
+ 'content-type': 'application/json',
137
+ 'x-iai-api-version': API_VERSION,
138
+ 'x-iai-event-time': new Date().toISOString(),
139
+ 'x-iai-event-type': 'productUpdated',
140
+ 'x-iai-event-uid': 'uid-123',
141
+ 'x-iai-object-type': 'product',
142
+ 'x-iai-panel-id': '1',
143
+ 'x-iai-signature': createHmac('sha256', SIGNATURE_KEY).update(rawBody).digest('hex'),
144
+ 'x-iai-webhook-time': new Date().toISOString(),
145
+ 'authorization': `Bearer ${TOKEN}`,
146
+ } as Record<string, string>;
147
+ const chain = webhooks
148
+ .validateHeaders((h) => h.token === TOKEN)
149
+ .validateSignature(SIGNATURE_KEY)
150
+ .on('productUpdated', mockHandler);
151
+ const result = await chain.handleRaw({ headers: rawHeaders, rawBody, body: requestBody });
152
+ expect(result).toEqual({ matched: true, eventType: 'productUpdated' });
153
+ expect(mockHandler).toHaveBeenCalledTimes(1);
154
+ const ctx = mockHandler.mock.calls[0][0];
155
+ expect(ctx.body).toEqual(requestBody);
156
+ expect(ctx.headers.objectType).toBe('product');
157
+ });
158
+ });