@miniduckco/stash 0.1.1 → 0.1.2
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/src/index.d.ts +1 -1
- package/dist/src/index.js +225 -20
- package/dist/src/types.d.ts +28 -0
- package/dist/test/stash.test.js +31 -0
- package/package.json +1 -1
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ParsedWebhook, Payment, PaymentCreateInput, PaymentVerifyInput, PaymentRequest, PaymentResponse, StashConfig, VerificationResult, WebhookParseInput, WebhookVerifyInput, WebhookVerifyResult } from "./types.js";
|
|
2
|
-
export type { OzowProviderOptions, ParsedWebhook, Payment, PaymentCreateInput, PaymentProvider, PaymentVerifyInput, PaymentRequest, PaymentResponse, PaystackProviderOptions, PayfastProviderOptions, ProviderOptions, StashConfig, VerificationResult, WebhookEvent, WebhookParseInput, WebhookVerifyInput, WebhookVerifyResult, } from "./types.js";
|
|
2
|
+
export type { OzowProviderOptions, ParsedWebhook, LogEvent, Logger, Payment, PaymentCreateInput, PaymentProvider, PaymentVerifyInput, PaymentRequest, PaymentResponse, PaystackProviderOptions, PayfastProviderOptions, ProviderOptions, StashConfig, VerificationResult, WebhookEvent, WebhookParseInput, WebhookVerifyInput, WebhookVerifyResult, } from "./types.js";
|
|
3
3
|
export { StashError } from "./errors.js";
|
|
4
4
|
export { buildFormEncoded, parseFormBody, parseFormEncoded, pairsToRecord } from "./internal/form.js";
|
|
5
5
|
export declare function createStash(config: StashConfig): {
|
package/dist/src/index.js
CHANGED
|
@@ -10,11 +10,36 @@ export { buildFormEncoded, parseFormBody, parseFormEncoded, pairsToRecord } from
|
|
|
10
10
|
export function createStash(config) {
|
|
11
11
|
const provider = config.provider;
|
|
12
12
|
const testMode = config.testMode ?? false;
|
|
13
|
+
const logger = config.logger;
|
|
14
|
+
const emit = (event) => {
|
|
15
|
+
if (!logger)
|
|
16
|
+
return;
|
|
17
|
+
logger.log({
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
...event,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
13
22
|
return {
|
|
14
23
|
payments: {
|
|
15
24
|
create: async (input) => {
|
|
25
|
+
const correlationId = randomUUID();
|
|
26
|
+
const startedAt = Date.now();
|
|
16
27
|
const currency = input.currency ?? config.defaults?.currency ?? "ZAR";
|
|
17
28
|
const amountNumber = Number(formatAmount(input.amount));
|
|
29
|
+
emit({
|
|
30
|
+
event: "payments.create.request",
|
|
31
|
+
provider,
|
|
32
|
+
action: "create",
|
|
33
|
+
stage: "request",
|
|
34
|
+
correlation_id: correlationId,
|
|
35
|
+
status: "success",
|
|
36
|
+
metadata: {
|
|
37
|
+
amount: input.amount,
|
|
38
|
+
currency,
|
|
39
|
+
reference: input.reference,
|
|
40
|
+
testMode,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
18
43
|
const paymentRequest = {
|
|
19
44
|
provider,
|
|
20
45
|
amount: input.amount,
|
|
@@ -30,38 +55,183 @@ export function createStash(config) {
|
|
|
30
55
|
secrets: buildSecrets(provider, config.credentials),
|
|
31
56
|
};
|
|
32
57
|
const adapter = providerAdapters[provider];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
try {
|
|
59
|
+
const response = await adapter.createPayment(paymentRequest);
|
|
60
|
+
const payment = {
|
|
61
|
+
id: randomUUID(),
|
|
62
|
+
status: "pending",
|
|
63
|
+
amount: amountNumber,
|
|
64
|
+
currency,
|
|
65
|
+
redirectUrl: response.redirectUrl,
|
|
66
|
+
provider,
|
|
67
|
+
providerRef: response.paymentRequestId,
|
|
68
|
+
correlationId,
|
|
69
|
+
raw: response.raw ?? response,
|
|
70
|
+
};
|
|
71
|
+
emit({
|
|
72
|
+
event: "payments.create.response",
|
|
73
|
+
provider,
|
|
74
|
+
action: "create",
|
|
75
|
+
stage: "response",
|
|
76
|
+
correlation_id: correlationId,
|
|
77
|
+
status: "success",
|
|
78
|
+
duration_ms: Date.now() - startedAt,
|
|
79
|
+
metadata: {
|
|
80
|
+
amount: amountNumber,
|
|
81
|
+
currency,
|
|
82
|
+
reference: input.reference,
|
|
83
|
+
provider_ref: response.paymentRequestId,
|
|
84
|
+
testMode,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return payment;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
emit({
|
|
91
|
+
event: "payments.create.error",
|
|
92
|
+
provider,
|
|
93
|
+
action: "create",
|
|
94
|
+
stage: "error",
|
|
95
|
+
correlation_id: correlationId,
|
|
96
|
+
status: "failure",
|
|
97
|
+
duration_ms: Date.now() - startedAt,
|
|
98
|
+
metadata: {
|
|
99
|
+
amount: input.amount,
|
|
100
|
+
currency,
|
|
101
|
+
reference: input.reference,
|
|
102
|
+
testMode,
|
|
103
|
+
},
|
|
104
|
+
error: {
|
|
105
|
+
code: error instanceof StashError ? error.code : "unknown",
|
|
106
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
44
111
|
},
|
|
45
112
|
verify: async (input) => {
|
|
113
|
+
const correlationId = randomUUID();
|
|
114
|
+
const startedAt = Date.now();
|
|
115
|
+
emit({
|
|
116
|
+
event: "payments.verify.request",
|
|
117
|
+
provider,
|
|
118
|
+
action: "verify",
|
|
119
|
+
stage: "request",
|
|
120
|
+
correlation_id: correlationId,
|
|
121
|
+
status: "success",
|
|
122
|
+
metadata: {
|
|
123
|
+
reference: input.reference,
|
|
124
|
+
testMode,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
46
127
|
const adapter = providerAdapters[provider];
|
|
47
128
|
if (!adapter?.verifyPayment) {
|
|
48
|
-
|
|
129
|
+
const error = new StashError("unsupported_capability", `payments.verify is not supported for ${provider}`);
|
|
130
|
+
emit({
|
|
131
|
+
event: "payments.verify.error",
|
|
132
|
+
provider,
|
|
133
|
+
action: "verify",
|
|
134
|
+
stage: "error",
|
|
135
|
+
correlation_id: correlationId,
|
|
136
|
+
status: "failure",
|
|
137
|
+
duration_ms: Date.now() - startedAt,
|
|
138
|
+
metadata: {
|
|
139
|
+
reference: input.reference,
|
|
140
|
+
testMode,
|
|
141
|
+
},
|
|
142
|
+
error: {
|
|
143
|
+
code: error.code,
|
|
144
|
+
message: error.message,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const result = await adapter.verifyPayment({
|
|
151
|
+
reference: input.reference,
|
|
152
|
+
secrets: buildSecrets(provider, config.credentials),
|
|
153
|
+
testMode,
|
|
154
|
+
});
|
|
155
|
+
const enriched = {
|
|
156
|
+
...result,
|
|
157
|
+
correlationId,
|
|
158
|
+
};
|
|
159
|
+
emit({
|
|
160
|
+
event: "payments.verify.response",
|
|
161
|
+
provider,
|
|
162
|
+
action: "verify",
|
|
163
|
+
stage: "response",
|
|
164
|
+
correlation_id: correlationId,
|
|
165
|
+
status: "success",
|
|
166
|
+
duration_ms: Date.now() - startedAt,
|
|
167
|
+
metadata: {
|
|
168
|
+
reference: input.reference,
|
|
169
|
+
provider_ref: result.providerRef,
|
|
170
|
+
testMode,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
return enriched;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
emit({
|
|
177
|
+
event: "payments.verify.error",
|
|
178
|
+
provider,
|
|
179
|
+
action: "verify",
|
|
180
|
+
stage: "error",
|
|
181
|
+
correlation_id: correlationId,
|
|
182
|
+
status: "failure",
|
|
183
|
+
duration_ms: Date.now() - startedAt,
|
|
184
|
+
metadata: {
|
|
185
|
+
reference: input.reference,
|
|
186
|
+
testMode,
|
|
187
|
+
},
|
|
188
|
+
error: {
|
|
189
|
+
code: error instanceof StashError ? error.code : "unknown",
|
|
190
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
throw error;
|
|
49
194
|
}
|
|
50
|
-
return adapter.verifyPayment({
|
|
51
|
-
reference: input.reference,
|
|
52
|
-
secrets: buildSecrets(provider, config.credentials),
|
|
53
|
-
testMode,
|
|
54
|
-
});
|
|
55
195
|
},
|
|
56
196
|
},
|
|
57
197
|
webhooks: {
|
|
58
198
|
parse: (input) => {
|
|
199
|
+
const correlationId = randomUUID();
|
|
200
|
+
const startedAt = Date.now();
|
|
59
201
|
const resolvedProvider = input.provider ?? provider;
|
|
60
202
|
const secrets = buildSecrets(resolvedProvider, config.credentials);
|
|
61
203
|
const rawBody = input.rawBody;
|
|
204
|
+
emit({
|
|
205
|
+
event: "webhooks.parse.request",
|
|
206
|
+
provider: resolvedProvider,
|
|
207
|
+
action: "parse",
|
|
208
|
+
stage: "request",
|
|
209
|
+
correlation_id: correlationId,
|
|
210
|
+
status: "success",
|
|
211
|
+
metadata: {
|
|
212
|
+
testMode,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
62
215
|
const adapter = providerAdapters[resolvedProvider];
|
|
63
216
|
if (!adapter) {
|
|
64
|
-
|
|
217
|
+
const error = new Error(`Unsupported provider: ${resolvedProvider}`);
|
|
218
|
+
emit({
|
|
219
|
+
event: "webhooks.parse.error",
|
|
220
|
+
provider: resolvedProvider,
|
|
221
|
+
action: "parse",
|
|
222
|
+
stage: "error",
|
|
223
|
+
correlation_id: correlationId,
|
|
224
|
+
status: "failure",
|
|
225
|
+
duration_ms: Date.now() - startedAt,
|
|
226
|
+
metadata: {
|
|
227
|
+
testMode,
|
|
228
|
+
},
|
|
229
|
+
error: {
|
|
230
|
+
code: "unsupported_provider",
|
|
231
|
+
message: error.message,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
throw error;
|
|
65
235
|
}
|
|
66
236
|
const parsed = adapter.parseWebhook({
|
|
67
237
|
rawBody,
|
|
@@ -69,13 +239,48 @@ export function createStash(config) {
|
|
|
69
239
|
secrets,
|
|
70
240
|
});
|
|
71
241
|
if (!parsed.isValid) {
|
|
72
|
-
|
|
242
|
+
const error = new StashError("invalid_signature", `Invalid ${resolvedProvider} signature`);
|
|
243
|
+
emit({
|
|
244
|
+
event: "webhooks.parse.error",
|
|
245
|
+
provider: resolvedProvider,
|
|
246
|
+
action: "parse",
|
|
247
|
+
stage: "error",
|
|
248
|
+
correlation_id: correlationId,
|
|
249
|
+
status: "failure",
|
|
250
|
+
duration_ms: Date.now() - startedAt,
|
|
251
|
+
metadata: {
|
|
252
|
+
testMode,
|
|
253
|
+
},
|
|
254
|
+
error: {
|
|
255
|
+
code: error.code,
|
|
256
|
+
message: error.message,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
throw error;
|
|
73
260
|
}
|
|
74
|
-
|
|
261
|
+
const response = {
|
|
75
262
|
event: parsed.event,
|
|
76
263
|
provider: resolvedProvider,
|
|
264
|
+
correlationId,
|
|
77
265
|
raw: parsed.raw,
|
|
78
266
|
};
|
|
267
|
+
emit({
|
|
268
|
+
event: "webhooks.parse.response",
|
|
269
|
+
provider: resolvedProvider,
|
|
270
|
+
action: "parse",
|
|
271
|
+
stage: "response",
|
|
272
|
+
correlation_id: correlationId,
|
|
273
|
+
status: "success",
|
|
274
|
+
duration_ms: Date.now() - startedAt,
|
|
275
|
+
metadata: {
|
|
276
|
+
amount: parsed.event.data.amount,
|
|
277
|
+
currency: parsed.event.data.currency,
|
|
278
|
+
reference: parsed.event.data.reference,
|
|
279
|
+
provider_ref: parsed.event.data.providerRef,
|
|
280
|
+
testMode,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
return response;
|
|
79
284
|
},
|
|
80
285
|
},
|
|
81
286
|
};
|
package/dist/src/types.d.ts
CHANGED
|
@@ -38,6 +38,31 @@ export type StashConfig = {
|
|
|
38
38
|
defaults?: {
|
|
39
39
|
currency?: string;
|
|
40
40
|
};
|
|
41
|
+
logger?: Logger;
|
|
42
|
+
};
|
|
43
|
+
export type LogEvent = {
|
|
44
|
+
event: string;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
provider: PaymentProvider;
|
|
47
|
+
action: "create" | "verify" | "parse";
|
|
48
|
+
stage: "request" | "response" | "error";
|
|
49
|
+
correlation_id: string;
|
|
50
|
+
status: "success" | "failure";
|
|
51
|
+
duration_ms?: number;
|
|
52
|
+
metadata?: {
|
|
53
|
+
amount?: number | string;
|
|
54
|
+
currency?: string;
|
|
55
|
+
reference?: string;
|
|
56
|
+
provider_ref?: string;
|
|
57
|
+
testMode?: boolean;
|
|
58
|
+
};
|
|
59
|
+
error?: {
|
|
60
|
+
code: string;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export type Logger = {
|
|
65
|
+
log: (event: LogEvent) => void;
|
|
41
66
|
};
|
|
42
67
|
export type PaymentCreateInput = {
|
|
43
68
|
amount: string | number;
|
|
@@ -68,6 +93,7 @@ export type Payment = {
|
|
|
68
93
|
redirectUrl?: string;
|
|
69
94
|
provider: PaymentProvider;
|
|
70
95
|
providerRef?: string;
|
|
96
|
+
correlationId?: string;
|
|
71
97
|
raw?: unknown;
|
|
72
98
|
};
|
|
73
99
|
export type PaymentVerifyInput = {
|
|
@@ -77,6 +103,7 @@ export type VerificationResult = {
|
|
|
77
103
|
provider: PaymentProvider;
|
|
78
104
|
status: "pending" | "paid" | "failed" | "unknown";
|
|
79
105
|
providerRef?: string;
|
|
106
|
+
correlationId?: string;
|
|
80
107
|
raw?: unknown;
|
|
81
108
|
};
|
|
82
109
|
export type WebhookEvent = {
|
|
@@ -99,6 +126,7 @@ export type WebhookParseInput = {
|
|
|
99
126
|
export type ParsedWebhook = {
|
|
100
127
|
event: WebhookEvent;
|
|
101
128
|
provider: PaymentProvider;
|
|
129
|
+
correlationId?: string;
|
|
102
130
|
raw: Record<string, unknown>;
|
|
103
131
|
};
|
|
104
132
|
export type PaymentRequest = {
|
package/dist/test/stash.test.js
CHANGED
|
@@ -22,6 +22,34 @@ test("createStash payments.create returns canonical payment", async () => {
|
|
|
22
22
|
assert.equal(payment.provider, "payfast");
|
|
23
23
|
assert.match(payment.id, /^[0-9a-f-]{36}$/i);
|
|
24
24
|
assert.ok(payment.redirectUrl);
|
|
25
|
+
assert.match(payment.correlationId ?? "", /^[0-9a-f-]{36}$/i);
|
|
26
|
+
});
|
|
27
|
+
test("createStash emits canonical logs for payments.create", async () => {
|
|
28
|
+
const events = [];
|
|
29
|
+
const stash = createStash({
|
|
30
|
+
provider: "payfast",
|
|
31
|
+
credentials: {
|
|
32
|
+
merchantId: "merchant",
|
|
33
|
+
merchantKey: "key",
|
|
34
|
+
},
|
|
35
|
+
testMode: true,
|
|
36
|
+
logger: {
|
|
37
|
+
log: (event) => events.push(event),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const payment = await stash.payments.create({
|
|
41
|
+
amount: "100.00",
|
|
42
|
+
currency: "ZAR",
|
|
43
|
+
reference: "ORDER-1",
|
|
44
|
+
});
|
|
45
|
+
assert.equal(events.length, 2);
|
|
46
|
+
assert.equal(events[0].event, "payments.create.request");
|
|
47
|
+
assert.equal(events[1].event, "payments.create.response");
|
|
48
|
+
assert.equal(events[0].provider, "payfast");
|
|
49
|
+
assert.equal(events[1].provider, "payfast");
|
|
50
|
+
assert.equal(events[0].correlation_id, payment.correlationId);
|
|
51
|
+
assert.equal(events[1].correlation_id, payment.correlationId);
|
|
52
|
+
assert.equal(events[1].metadata.testMode, true);
|
|
25
53
|
});
|
|
26
54
|
test("createStash payments.create maps ozow response", async () => {
|
|
27
55
|
const originalFetch = globalThis.fetch;
|
|
@@ -127,6 +155,7 @@ test("webhooks.parse returns canonical event for payfast", () => {
|
|
|
127
155
|
assert.equal(parsed.event.type, "payment.completed");
|
|
128
156
|
assert.equal(parsed.event.data.reference, "ORDER-100");
|
|
129
157
|
assert.equal(parsed.provider, "payfast");
|
|
158
|
+
assert.match(parsed.correlationId ?? "", /^[0-9a-f-]{36}$/i);
|
|
130
159
|
});
|
|
131
160
|
test("webhooks.parse throws invalid_signature for payfast", () => {
|
|
132
161
|
const stash = createStash({
|
|
@@ -172,6 +201,7 @@ test("webhooks.parse returns canonical event for paystack", () => {
|
|
|
172
201
|
assert.equal(parsed.provider, "paystack");
|
|
173
202
|
assert.equal(parsed.event.type, "payment.completed");
|
|
174
203
|
assert.equal(parsed.event.data.reference, "REF-3");
|
|
204
|
+
assert.match(parsed.correlationId ?? "", /^[0-9a-f-]{36}$/i);
|
|
175
205
|
});
|
|
176
206
|
test("payments.verify throws unsupported_capability for payfast", async () => {
|
|
177
207
|
const stash = createStash({
|
|
@@ -205,5 +235,6 @@ test("payments.verify returns paid for paystack", async () => {
|
|
|
205
235
|
});
|
|
206
236
|
const result = await stash.payments.verify({ reference: "REF-1" });
|
|
207
237
|
assert.equal(result.status, "paid");
|
|
238
|
+
assert.match(result.correlationId ?? "", /^[0-9a-f-]{36}$/i);
|
|
208
239
|
globalThis.fetch = originalFetch;
|
|
209
240
|
});
|