@progus/connector 0.1.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/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @progus/connector
2
+
3
+ Headless connector for Progus partner/affiliate tracking, partner ID assignment,
4
+ and cross-sell offers. No UI code, no embedded secrets.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install @progus/connector
10
+ ```
11
+
12
+ ## Usage (Shopify app, Node/TS)
13
+
14
+ ```ts
15
+ import { createProgusConnector } from "@progus/connector";
16
+
17
+ const connector = createProgusConnector({
18
+ appKey: "progus-store-locator",
19
+ apiBaseUrl: process.env.PARTNERS_API_URL!,
20
+ signingSecret: process.env.PARTNERS_SECRET_KEY!,
21
+ apiKey: process.env.PARTNERS_API_KEY, // optional
22
+ });
23
+
24
+ await connector.trackInstall({
25
+ shopDomain: session.shop,
26
+ partnerId: partnerIdFromCookie,
27
+ });
28
+
29
+ await connector.trackSubscriptionUpdated({
30
+ shopDomain: session.shop,
31
+ data: {
32
+ subscriptionStatus: "ACTIVE",
33
+ subscriptionName: "pro",
34
+ subscriptionId: "gid://shopify/AppSubscription/123",
35
+ },
36
+ });
37
+
38
+ await connector.trackUninstall({ shopDomain: session.shop });
39
+ ```
40
+
41
+ ## Partner ID assignment
42
+
43
+ ```ts
44
+ const result = await connector.assignPartnerId({
45
+ shopDomain: "my-shop.myshopify.com",
46
+ partnerId: "partner_123",
47
+ });
48
+
49
+ if (result.success) {
50
+ // persist partnerId in your app (db/cookie) as needed
51
+ }
52
+ ```
53
+
54
+ ## Cross-sell offers
55
+
56
+ ```ts
57
+ import { getCrossSellOffers } from "@progus/connector";
58
+
59
+ const offers = getCrossSellOffers({
60
+ currentAppKey: "progus-store-locator",
61
+ installedAppKeys: ["progus_cod"],
62
+ });
63
+ ```
64
+
65
+ ## Optional signing helper
66
+
67
+ ```ts
68
+ import { signPayload } from "@progus/connector";
69
+
70
+ const signature = signPayload(JSON.stringify({ test: true }), process.env.PARTNERS_SECRET_KEY!);
71
+ ```
72
+
73
+ ## Environment variables
74
+
75
+ - `PARTNERS_API_URL` - base URL for partner API (e.g. `https://partners.example.com`)
76
+ - `PARTNERS_SECRET_KEY` - signing secret for HMAC (required for tracking)
77
+ - `PARTNERS_API_KEY` - optional API key header
78
+
79
+ ## Publishing
80
+
81
+ ```bash
82
+ npm run build
83
+ npm publish --access public
84
+ ```
85
+
86
+ ## Smoke script
87
+
88
+ ```bash
89
+ npm run smoke
90
+ ```
91
+
92
+ ## Migration notes (Store Locator)
93
+
94
+ Found integrations and call sites:
95
+ - `web/controllers/partners.controller.js` (partner events + signing)
96
+ - `web/controllers/auth/shopifyAuth.controller.js` (install event on OAuth)
97
+ - `web/controllers/store.controller.js` (uninstall event)
98
+ - `web/controllers/subscription.controller.js` (subscription event)
99
+ - `web/routes/index.route.js` (save/check partner ID API)
100
+ - `web/frontend/pages/plans.jsx` (partner ID UI)
101
+ - `web/frontend/components/DashboardComponents/RecommendationsCard.jsx` (cross-sell)
102
+ - `web/frontend/constants/recommendations.js` (apps catalog, moved to connector)
package/dist/index.cjs ADDED
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ appsCatalog: () => appsCatalog,
24
+ createProgusConnector: () => createProgusConnector,
25
+ getCrossSellOffers: () => getCrossSellOffers,
26
+ normalizePartnerId: () => normalizePartnerId,
27
+ signPayload: () => signPayload
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/signing.ts
32
+ var import_crypto = require("crypto");
33
+ function signPayload(body, secret) {
34
+ return (0, import_crypto.createHmac)("sha256", secret).update(body).digest("hex");
35
+ }
36
+
37
+ // src/utils.ts
38
+ function normalizePartnerId(value) {
39
+ if (!value) return null;
40
+ const trimmed = value.trim();
41
+ return trimmed.length > 0 ? trimmed : null;
42
+ }
43
+ function buildEventId(shopDomain, eventName, externalId) {
44
+ if (!externalId) return void 0;
45
+ return `${shopDomain}:${eventName}:${externalId}`;
46
+ }
47
+ function stripTrailingSlash(value) {
48
+ return value.replace(/\/+$/, "");
49
+ }
50
+ function safeJsonParse(text) {
51
+ try {
52
+ return JSON.parse(text);
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ // src/connector.ts
59
+ function createProgusConnector(config) {
60
+ const apiBaseUrl = config.apiBaseUrl ? stripTrailingSlash(config.apiBaseUrl) : "";
61
+ const fetchImpl = config.fetch ?? globalThis.fetch;
62
+ const logger = config.logger ?? console;
63
+ const enableIdempotency = config.enableIdempotency !== false;
64
+ if (!fetchImpl) {
65
+ throw new Error("Fetch implementation is required");
66
+ }
67
+ async function postEvent(eventName, payload) {
68
+ const { shopDomain, partnerId, data, externalId } = payload;
69
+ if (!shopDomain) {
70
+ logger?.info?.("Partner event skipped: missing customerId");
71
+ return { success: false, message: "Missing customerId" };
72
+ }
73
+ if (!apiBaseUrl || !config.signingSecret) {
74
+ logger?.info?.("Partner event skipped: missing API configuration");
75
+ return { success: false, message: "Missing API configuration" };
76
+ }
77
+ const requestPayload = {
78
+ customerId: shopDomain,
79
+ appName: config.appKey,
80
+ data: data ?? {}
81
+ };
82
+ if (partnerId) {
83
+ requestPayload.partnerId = partnerId;
84
+ }
85
+ if (enableIdempotency) {
86
+ const eventId = buildEventId(shopDomain, String(eventName), externalId);
87
+ if (eventId) {
88
+ requestPayload.eventId = eventId;
89
+ }
90
+ }
91
+ const requestBody = JSON.stringify(requestPayload);
92
+ const headers = {
93
+ "Content-Type": "application/json",
94
+ "x-progus-hmac-sha256": signPayload(requestBody, config.signingSecret)
95
+ };
96
+ if (config.apiKey) {
97
+ headers["x-api-key"] = config.apiKey;
98
+ }
99
+ try {
100
+ const response = await fetchImpl(`${apiBaseUrl}/event/${eventName}`, {
101
+ method: "POST",
102
+ headers,
103
+ body: requestBody
104
+ });
105
+ const text = await response.text();
106
+ const parsed = safeJsonParse(text);
107
+ if (!response.ok) {
108
+ logger?.error?.(`Failed to send ${String(eventName)} event`, {
109
+ status: response.status
110
+ });
111
+ return {
112
+ success: false,
113
+ message: parsed?.message ?? `Failed to send ${String(eventName)} event`,
114
+ status: response.status,
115
+ data: parsed ?? void 0
116
+ };
117
+ }
118
+ return {
119
+ success: parsed?.success ?? true,
120
+ message: parsed?.message ?? `Successfully sent ${String(eventName)} event`,
121
+ status: response.status,
122
+ data: parsed ?? void 0
123
+ };
124
+ } catch (error) {
125
+ logger?.error?.(`Error sending ${String(eventName)} event`, {
126
+ error: error instanceof Error ? error.message : String(error)
127
+ });
128
+ return {
129
+ success: false,
130
+ message: error instanceof Error ? error.message : "Error sending event"
131
+ };
132
+ }
133
+ }
134
+ async function trackInstall(payload) {
135
+ const partnerId = normalizePartnerId(payload.partnerId);
136
+ if (!partnerId) {
137
+ logger?.info?.("Partner event skipped: missing partnerId required for installation event");
138
+ return { success: false, message: "Missing partnerId" };
139
+ }
140
+ return postEvent("installation", { shopDomain: payload.shopDomain, partnerId });
141
+ }
142
+ async function trackUninstall(payload) {
143
+ return postEvent("uninstallation", { shopDomain: payload.shopDomain });
144
+ }
145
+ async function trackSubscription(payload) {
146
+ return postEvent("subscription", {
147
+ shopDomain: payload.shopDomain,
148
+ data: payload.data,
149
+ externalId: payload.data.subscriptionId
150
+ });
151
+ }
152
+ async function assignPartnerId(payload) {
153
+ const normalized = normalizePartnerId(payload.partnerId);
154
+ if (!normalized) {
155
+ return { success: false, message: "Partner ID is required" };
156
+ }
157
+ const result = await trackInstall({ shopDomain: payload.shopDomain, partnerId: normalized });
158
+ return { ...result, partnerId: normalized };
159
+ }
160
+ async function checkPartnerId(payload) {
161
+ if (!payload.shopDomain) {
162
+ return { success: false, message: "Missing shop domain" };
163
+ }
164
+ if (!apiBaseUrl || !config.signingSecret) {
165
+ return { success: false, message: "Partners API configuration not set", status: 500 };
166
+ }
167
+ const requestBody = JSON.stringify({ shop: payload.shopDomain });
168
+ const headers = {
169
+ "Content-Type": "application/json",
170
+ "x-progus-hmac-sha256": signPayload(requestBody, config.signingSecret)
171
+ };
172
+ if (config.apiKey) {
173
+ headers["x-api-key"] = config.apiKey;
174
+ }
175
+ try {
176
+ const response = await fetchImpl(`${apiBaseUrl}/event/checkPartnerId`, {
177
+ method: "POST",
178
+ headers,
179
+ body: requestBody
180
+ });
181
+ const text = await response.text();
182
+ const parsed = safeJsonParse(text);
183
+ if (response.ok && parsed) {
184
+ return {
185
+ success: parsed.success ?? true,
186
+ partnerId: parsed.partnerId,
187
+ status: response.status,
188
+ message: parsed.message
189
+ };
190
+ }
191
+ logger?.error?.("checkPartnerId error", { status: response.status });
192
+ return { success: false, message: "Error checking partner ID", status: response.status };
193
+ } catch (error) {
194
+ logger?.error?.("Error checking partner ID", {
195
+ error: error instanceof Error ? error.message : String(error)
196
+ });
197
+ return { success: false, message: "Error checking partner ID", status: 500 };
198
+ }
199
+ }
200
+ return {
201
+ track: (eventName, payload) => postEvent(eventName, payload),
202
+ trackInstall,
203
+ trackUninstall,
204
+ trackSubscriptionPurchased: trackSubscription,
205
+ trackSubscriptionUpdated: trackSubscription,
206
+ trackSubscriptionCancelled: trackSubscription,
207
+ assignPartnerId,
208
+ checkPartnerId
209
+ };
210
+ }
211
+
212
+ // src/appsCatalog.json
213
+ var appsCatalog_default = [
214
+ {
215
+ key: "progus_cod",
216
+ type: "app",
217
+ title: "Progus COD",
218
+ company: "Progus",
219
+ companyUrl: "https://progus.com",
220
+ desc: "Automate COD Fees & Hide/Show Cash on Delivery by Rules",
221
+ url: "https://apps.shopify.com/progus-cod",
222
+ icon: "https://cdn.shopify.com/app-store/listing_images/bc537219cc3ed2bd4e7e3e683fe6b74a/icon/CMi_6dTEkIoDEAE=.png",
223
+ priority: 100,
224
+ enabled: true
225
+ },
226
+ {
227
+ key: "progus_trust_badges",
228
+ type: "app",
229
+ title: "Progus Trust Badges",
230
+ company: "Progus",
231
+ companyUrl: "https://progus.com",
232
+ desc: "Add Trust Badges to your store to build trust and credibility.",
233
+ url: "https://apps.shopify.com/progus-trust-badges-1",
234
+ icon: "https://cdn.shopify.com/app-store/listing_images/f9d0009e237f27d2db35b41ef99be858/icon/CJ3y1qDn1JEDEAE=.png",
235
+ priority: 90,
236
+ enabled: true
237
+ }
238
+ ];
239
+
240
+ // src/crossSell.ts
241
+ var catalog = appsCatalog_default;
242
+ function getCrossSellOffers(options = {}) {
243
+ const appsCatalog2 = options.appsCatalog ?? catalog;
244
+ const installedKeys = new Set(
245
+ [options.currentAppKey, ...options.installedAppKeys ?? []].filter(
246
+ (key) => Boolean(key)
247
+ )
248
+ );
249
+ const locale = options.locale;
250
+ return appsCatalog2.filter((app) => app.enabled !== false).filter((app) => locale ? !app.locales || app.locales.includes(locale) : true).filter((app) => !installedKeys.has(app.key)).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
251
+ }
252
+ var appsCatalog = catalog;
253
+ // Annotate the CommonJS export names for ESM import in node:
254
+ 0 && (module.exports = {
255
+ appsCatalog,
256
+ createProgusConnector,
257
+ getCrossSellOffers,
258
+ normalizePartnerId,
259
+ signPayload
260
+ });
@@ -0,0 +1,106 @@
1
+ type FetchLike = typeof fetch;
2
+ type Logger = {
3
+ info?: (message: string, meta?: Record<string, unknown>) => void;
4
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
5
+ error?: (message: string, meta?: Record<string, unknown>) => void;
6
+ };
7
+ type AppsCatalogEntry = {
8
+ key: string;
9
+ type: string;
10
+ title: string;
11
+ company?: string;
12
+ companyUrl?: string;
13
+ desc?: string;
14
+ url: string;
15
+ icon?: string;
16
+ enabled?: boolean;
17
+ priority?: number;
18
+ locales?: string[];
19
+ };
20
+ type CrossSellOptions = {
21
+ currentAppKey?: string;
22
+ installedAppKeys?: string[];
23
+ locale?: string;
24
+ shopPlan?: string;
25
+ appsCatalog?: AppsCatalogEntry[];
26
+ };
27
+ type ConnectorConfig = {
28
+ appKey: string;
29
+ apiBaseUrl: string;
30
+ apiKey?: string;
31
+ signingSecret?: string;
32
+ appsCatalog?: AppsCatalogEntry[];
33
+ fetch?: FetchLike;
34
+ logger?: Logger;
35
+ enableIdempotency?: boolean;
36
+ };
37
+ type TrackEventName = "installation" | "uninstallation" | "subscription";
38
+ type TrackEventParams<TData = Record<string, unknown>> = {
39
+ eventName: TrackEventName | (string & {});
40
+ shopDomain: string;
41
+ partnerId?: string | null;
42
+ data?: TData;
43
+ externalId?: string;
44
+ };
45
+ type TrackResult<TData = Record<string, unknown>> = {
46
+ success: boolean;
47
+ message?: string;
48
+ status?: number;
49
+ data?: TData;
50
+ };
51
+ type CheckPartnerIdResult = {
52
+ success: boolean;
53
+ partnerId?: string;
54
+ message?: string;
55
+ status?: number;
56
+ };
57
+ type AssignPartnerIdInput = {
58
+ shopDomain: string;
59
+ partnerId: string;
60
+ };
61
+ type SubscriptionEventData = {
62
+ subscriptionStatus?: string;
63
+ subscriptionName?: string;
64
+ subscriptionId?: string;
65
+ subscriptionPrice?: number;
66
+ subscriptionPeriod?: string;
67
+ };
68
+
69
+ type Connector = {
70
+ track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
71
+ trackInstall: (payload: {
72
+ shopDomain: string;
73
+ partnerId?: string | null;
74
+ }) => Promise<TrackResult>;
75
+ trackUninstall: (payload: {
76
+ shopDomain: string;
77
+ }) => Promise<TrackResult>;
78
+ trackSubscriptionPurchased: (payload: {
79
+ shopDomain: string;
80
+ data: SubscriptionEventData;
81
+ }) => Promise<TrackResult>;
82
+ trackSubscriptionUpdated: (payload: {
83
+ shopDomain: string;
84
+ data: SubscriptionEventData;
85
+ }) => Promise<TrackResult>;
86
+ trackSubscriptionCancelled: (payload: {
87
+ shopDomain: string;
88
+ data: SubscriptionEventData;
89
+ }) => Promise<TrackResult>;
90
+ assignPartnerId: (payload: AssignPartnerIdInput) => Promise<TrackResult & {
91
+ partnerId?: string;
92
+ }>;
93
+ checkPartnerId: (payload: {
94
+ shopDomain: string;
95
+ }) => Promise<CheckPartnerIdResult>;
96
+ };
97
+ declare function createProgusConnector(config: ConnectorConfig): Connector;
98
+
99
+ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntry[];
100
+ declare const appsCatalog: AppsCatalogEntry[];
101
+
102
+ declare function signPayload(body: string, secret: string): string;
103
+
104
+ declare function normalizePartnerId(value?: string | null): string | null;
105
+
106
+ export { type AppsCatalogEntry, type AssignPartnerIdInput, type CheckPartnerIdResult, type ConnectorConfig, type CrossSellOptions, type Logger, type SubscriptionEventData, type TrackEventName, type TrackEventParams, type TrackResult, appsCatalog, createProgusConnector, getCrossSellOffers, normalizePartnerId, signPayload };
@@ -0,0 +1,106 @@
1
+ type FetchLike = typeof fetch;
2
+ type Logger = {
3
+ info?: (message: string, meta?: Record<string, unknown>) => void;
4
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
5
+ error?: (message: string, meta?: Record<string, unknown>) => void;
6
+ };
7
+ type AppsCatalogEntry = {
8
+ key: string;
9
+ type: string;
10
+ title: string;
11
+ company?: string;
12
+ companyUrl?: string;
13
+ desc?: string;
14
+ url: string;
15
+ icon?: string;
16
+ enabled?: boolean;
17
+ priority?: number;
18
+ locales?: string[];
19
+ };
20
+ type CrossSellOptions = {
21
+ currentAppKey?: string;
22
+ installedAppKeys?: string[];
23
+ locale?: string;
24
+ shopPlan?: string;
25
+ appsCatalog?: AppsCatalogEntry[];
26
+ };
27
+ type ConnectorConfig = {
28
+ appKey: string;
29
+ apiBaseUrl: string;
30
+ apiKey?: string;
31
+ signingSecret?: string;
32
+ appsCatalog?: AppsCatalogEntry[];
33
+ fetch?: FetchLike;
34
+ logger?: Logger;
35
+ enableIdempotency?: boolean;
36
+ };
37
+ type TrackEventName = "installation" | "uninstallation" | "subscription";
38
+ type TrackEventParams<TData = Record<string, unknown>> = {
39
+ eventName: TrackEventName | (string & {});
40
+ shopDomain: string;
41
+ partnerId?: string | null;
42
+ data?: TData;
43
+ externalId?: string;
44
+ };
45
+ type TrackResult<TData = Record<string, unknown>> = {
46
+ success: boolean;
47
+ message?: string;
48
+ status?: number;
49
+ data?: TData;
50
+ };
51
+ type CheckPartnerIdResult = {
52
+ success: boolean;
53
+ partnerId?: string;
54
+ message?: string;
55
+ status?: number;
56
+ };
57
+ type AssignPartnerIdInput = {
58
+ shopDomain: string;
59
+ partnerId: string;
60
+ };
61
+ type SubscriptionEventData = {
62
+ subscriptionStatus?: string;
63
+ subscriptionName?: string;
64
+ subscriptionId?: string;
65
+ subscriptionPrice?: number;
66
+ subscriptionPeriod?: string;
67
+ };
68
+
69
+ type Connector = {
70
+ track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
71
+ trackInstall: (payload: {
72
+ shopDomain: string;
73
+ partnerId?: string | null;
74
+ }) => Promise<TrackResult>;
75
+ trackUninstall: (payload: {
76
+ shopDomain: string;
77
+ }) => Promise<TrackResult>;
78
+ trackSubscriptionPurchased: (payload: {
79
+ shopDomain: string;
80
+ data: SubscriptionEventData;
81
+ }) => Promise<TrackResult>;
82
+ trackSubscriptionUpdated: (payload: {
83
+ shopDomain: string;
84
+ data: SubscriptionEventData;
85
+ }) => Promise<TrackResult>;
86
+ trackSubscriptionCancelled: (payload: {
87
+ shopDomain: string;
88
+ data: SubscriptionEventData;
89
+ }) => Promise<TrackResult>;
90
+ assignPartnerId: (payload: AssignPartnerIdInput) => Promise<TrackResult & {
91
+ partnerId?: string;
92
+ }>;
93
+ checkPartnerId: (payload: {
94
+ shopDomain: string;
95
+ }) => Promise<CheckPartnerIdResult>;
96
+ };
97
+ declare function createProgusConnector(config: ConnectorConfig): Connector;
98
+
99
+ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntry[];
100
+ declare const appsCatalog: AppsCatalogEntry[];
101
+
102
+ declare function signPayload(body: string, secret: string): string;
103
+
104
+ declare function normalizePartnerId(value?: string | null): string | null;
105
+
106
+ export { type AppsCatalogEntry, type AssignPartnerIdInput, type CheckPartnerIdResult, type ConnectorConfig, type CrossSellOptions, type Logger, type SubscriptionEventData, type TrackEventName, type TrackEventParams, type TrackResult, appsCatalog, createProgusConnector, getCrossSellOffers, normalizePartnerId, signPayload };
package/dist/index.js ADDED
@@ -0,0 +1,229 @@
1
+ // src/signing.ts
2
+ import { createHmac } from "crypto";
3
+ function signPayload(body, secret) {
4
+ return createHmac("sha256", secret).update(body).digest("hex");
5
+ }
6
+
7
+ // src/utils.ts
8
+ function normalizePartnerId(value) {
9
+ if (!value) return null;
10
+ const trimmed = value.trim();
11
+ return trimmed.length > 0 ? trimmed : null;
12
+ }
13
+ function buildEventId(shopDomain, eventName, externalId) {
14
+ if (!externalId) return void 0;
15
+ return `${shopDomain}:${eventName}:${externalId}`;
16
+ }
17
+ function stripTrailingSlash(value) {
18
+ return value.replace(/\/+$/, "");
19
+ }
20
+ function safeJsonParse(text) {
21
+ try {
22
+ return JSON.parse(text);
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ // src/connector.ts
29
+ function createProgusConnector(config) {
30
+ const apiBaseUrl = config.apiBaseUrl ? stripTrailingSlash(config.apiBaseUrl) : "";
31
+ const fetchImpl = config.fetch ?? globalThis.fetch;
32
+ const logger = config.logger ?? console;
33
+ const enableIdempotency = config.enableIdempotency !== false;
34
+ if (!fetchImpl) {
35
+ throw new Error("Fetch implementation is required");
36
+ }
37
+ async function postEvent(eventName, payload) {
38
+ const { shopDomain, partnerId, data, externalId } = payload;
39
+ if (!shopDomain) {
40
+ logger?.info?.("Partner event skipped: missing customerId");
41
+ return { success: false, message: "Missing customerId" };
42
+ }
43
+ if (!apiBaseUrl || !config.signingSecret) {
44
+ logger?.info?.("Partner event skipped: missing API configuration");
45
+ return { success: false, message: "Missing API configuration" };
46
+ }
47
+ const requestPayload = {
48
+ customerId: shopDomain,
49
+ appName: config.appKey,
50
+ data: data ?? {}
51
+ };
52
+ if (partnerId) {
53
+ requestPayload.partnerId = partnerId;
54
+ }
55
+ if (enableIdempotency) {
56
+ const eventId = buildEventId(shopDomain, String(eventName), externalId);
57
+ if (eventId) {
58
+ requestPayload.eventId = eventId;
59
+ }
60
+ }
61
+ const requestBody = JSON.stringify(requestPayload);
62
+ const headers = {
63
+ "Content-Type": "application/json",
64
+ "x-progus-hmac-sha256": signPayload(requestBody, config.signingSecret)
65
+ };
66
+ if (config.apiKey) {
67
+ headers["x-api-key"] = config.apiKey;
68
+ }
69
+ try {
70
+ const response = await fetchImpl(`${apiBaseUrl}/event/${eventName}`, {
71
+ method: "POST",
72
+ headers,
73
+ body: requestBody
74
+ });
75
+ const text = await response.text();
76
+ const parsed = safeJsonParse(text);
77
+ if (!response.ok) {
78
+ logger?.error?.(`Failed to send ${String(eventName)} event`, {
79
+ status: response.status
80
+ });
81
+ return {
82
+ success: false,
83
+ message: parsed?.message ?? `Failed to send ${String(eventName)} event`,
84
+ status: response.status,
85
+ data: parsed ?? void 0
86
+ };
87
+ }
88
+ return {
89
+ success: parsed?.success ?? true,
90
+ message: parsed?.message ?? `Successfully sent ${String(eventName)} event`,
91
+ status: response.status,
92
+ data: parsed ?? void 0
93
+ };
94
+ } catch (error) {
95
+ logger?.error?.(`Error sending ${String(eventName)} event`, {
96
+ error: error instanceof Error ? error.message : String(error)
97
+ });
98
+ return {
99
+ success: false,
100
+ message: error instanceof Error ? error.message : "Error sending event"
101
+ };
102
+ }
103
+ }
104
+ async function trackInstall(payload) {
105
+ const partnerId = normalizePartnerId(payload.partnerId);
106
+ if (!partnerId) {
107
+ logger?.info?.("Partner event skipped: missing partnerId required for installation event");
108
+ return { success: false, message: "Missing partnerId" };
109
+ }
110
+ return postEvent("installation", { shopDomain: payload.shopDomain, partnerId });
111
+ }
112
+ async function trackUninstall(payload) {
113
+ return postEvent("uninstallation", { shopDomain: payload.shopDomain });
114
+ }
115
+ async function trackSubscription(payload) {
116
+ return postEvent("subscription", {
117
+ shopDomain: payload.shopDomain,
118
+ data: payload.data,
119
+ externalId: payload.data.subscriptionId
120
+ });
121
+ }
122
+ async function assignPartnerId(payload) {
123
+ const normalized = normalizePartnerId(payload.partnerId);
124
+ if (!normalized) {
125
+ return { success: false, message: "Partner ID is required" };
126
+ }
127
+ const result = await trackInstall({ shopDomain: payload.shopDomain, partnerId: normalized });
128
+ return { ...result, partnerId: normalized };
129
+ }
130
+ async function checkPartnerId(payload) {
131
+ if (!payload.shopDomain) {
132
+ return { success: false, message: "Missing shop domain" };
133
+ }
134
+ if (!apiBaseUrl || !config.signingSecret) {
135
+ return { success: false, message: "Partners API configuration not set", status: 500 };
136
+ }
137
+ const requestBody = JSON.stringify({ shop: payload.shopDomain });
138
+ const headers = {
139
+ "Content-Type": "application/json",
140
+ "x-progus-hmac-sha256": signPayload(requestBody, config.signingSecret)
141
+ };
142
+ if (config.apiKey) {
143
+ headers["x-api-key"] = config.apiKey;
144
+ }
145
+ try {
146
+ const response = await fetchImpl(`${apiBaseUrl}/event/checkPartnerId`, {
147
+ method: "POST",
148
+ headers,
149
+ body: requestBody
150
+ });
151
+ const text = await response.text();
152
+ const parsed = safeJsonParse(text);
153
+ if (response.ok && parsed) {
154
+ return {
155
+ success: parsed.success ?? true,
156
+ partnerId: parsed.partnerId,
157
+ status: response.status,
158
+ message: parsed.message
159
+ };
160
+ }
161
+ logger?.error?.("checkPartnerId error", { status: response.status });
162
+ return { success: false, message: "Error checking partner ID", status: response.status };
163
+ } catch (error) {
164
+ logger?.error?.("Error checking partner ID", {
165
+ error: error instanceof Error ? error.message : String(error)
166
+ });
167
+ return { success: false, message: "Error checking partner ID", status: 500 };
168
+ }
169
+ }
170
+ return {
171
+ track: (eventName, payload) => postEvent(eventName, payload),
172
+ trackInstall,
173
+ trackUninstall,
174
+ trackSubscriptionPurchased: trackSubscription,
175
+ trackSubscriptionUpdated: trackSubscription,
176
+ trackSubscriptionCancelled: trackSubscription,
177
+ assignPartnerId,
178
+ checkPartnerId
179
+ };
180
+ }
181
+
182
+ // src/appsCatalog.json
183
+ var appsCatalog_default = [
184
+ {
185
+ key: "progus_cod",
186
+ type: "app",
187
+ title: "Progus COD",
188
+ company: "Progus",
189
+ companyUrl: "https://progus.com",
190
+ desc: "Automate COD Fees & Hide/Show Cash on Delivery by Rules",
191
+ url: "https://apps.shopify.com/progus-cod",
192
+ icon: "https://cdn.shopify.com/app-store/listing_images/bc537219cc3ed2bd4e7e3e683fe6b74a/icon/CMi_6dTEkIoDEAE=.png",
193
+ priority: 100,
194
+ enabled: true
195
+ },
196
+ {
197
+ key: "progus_trust_badges",
198
+ type: "app",
199
+ title: "Progus Trust Badges",
200
+ company: "Progus",
201
+ companyUrl: "https://progus.com",
202
+ desc: "Add Trust Badges to your store to build trust and credibility.",
203
+ url: "https://apps.shopify.com/progus-trust-badges-1",
204
+ icon: "https://cdn.shopify.com/app-store/listing_images/f9d0009e237f27d2db35b41ef99be858/icon/CJ3y1qDn1JEDEAE=.png",
205
+ priority: 90,
206
+ enabled: true
207
+ }
208
+ ];
209
+
210
+ // src/crossSell.ts
211
+ var catalog = appsCatalog_default;
212
+ function getCrossSellOffers(options = {}) {
213
+ const appsCatalog2 = options.appsCatalog ?? catalog;
214
+ const installedKeys = new Set(
215
+ [options.currentAppKey, ...options.installedAppKeys ?? []].filter(
216
+ (key) => Boolean(key)
217
+ )
218
+ );
219
+ const locale = options.locale;
220
+ return appsCatalog2.filter((app) => app.enabled !== false).filter((app) => locale ? !app.locales || app.locales.includes(locale) : true).filter((app) => !installedKeys.has(app.key)).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
221
+ }
222
+ var appsCatalog = catalog;
223
+ export {
224
+ appsCatalog,
225
+ createProgusConnector,
226
+ getCrossSellOffers,
227
+ normalizePartnerId,
228
+ signPayload
229
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@progus/connector",
3
+ "version": "0.1.0",
4
+ "description": "Progus partner/affiliate connector helpers",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "src/appsCatalog.json"
21
+ ],
22
+ "sideEffects": false,
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --target node18",
31
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch --target node18",
32
+ "smoke": "tsx scripts/smoke.ts"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.11.30",
36
+ "tsup": "^8.0.1",
37
+ "tsx": "^4.7.1",
38
+ "typescript": "^5.4.5"
39
+ }
40
+ }
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "key": "progus_cod",
4
+ "type": "app",
5
+ "title": "Progus COD",
6
+ "company": "Progus",
7
+ "companyUrl": "https://progus.com",
8
+ "desc": "Automate COD Fees & Hide/Show Cash on Delivery by Rules",
9
+ "url": "https://apps.shopify.com/progus-cod",
10
+ "icon": "https://cdn.shopify.com/app-store/listing_images/bc537219cc3ed2bd4e7e3e683fe6b74a/icon/CMi_6dTEkIoDEAE=.png",
11
+ "priority": 100,
12
+ "enabled": true
13
+ },
14
+ {
15
+ "key": "progus_trust_badges",
16
+ "type": "app",
17
+ "title": "Progus Trust Badges",
18
+ "company": "Progus",
19
+ "companyUrl": "https://progus.com",
20
+ "desc": "Add Trust Badges to your store to build trust and credibility.",
21
+ "url": "https://apps.shopify.com/progus-trust-badges-1",
22
+ "icon": "https://cdn.shopify.com/app-store/listing_images/f9d0009e237f27d2db35b41ef99be858/icon/CJ3y1qDn1JEDEAE=.png",
23
+ "priority": 90,
24
+ "enabled": true
25
+ }
26
+ ]