@secureprocessing/node 0.0.0-stage → 0.2.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.
@@ -0,0 +1,112 @@
1
+ /** The shape every non-2xx response from the API has. */
2
+ export interface ApiErrorBody {
3
+ error: {
4
+ type: string;
5
+ code: string;
6
+ message: string;
7
+ param?: string;
8
+ decline_code?: string;
9
+ request_id?: string;
10
+ /** On `403 insufficient_permissions`: the permission the request needed, e.g. `payments:refund`. */
11
+ required_capability?: string;
12
+ };
13
+ }
14
+ /**
15
+ * Something the API refused.
16
+ *
17
+ * Branch on `type` for the category and `code` for the specific reason. For a declined card,
18
+ * `declineCode` says why in terms you can show a customer. `requestId` is what to quote to support.
19
+ * `param` names the field at fault — including a `404 resource_missing` for an id in the body that
20
+ * is not yours (`param: "customer"`). A `403` names the missing permission in `requiredCapability`.
21
+ */
22
+ export declare class SecureProcessingError extends Error {
23
+ readonly type: string;
24
+ readonly code: string;
25
+ readonly param: string | undefined;
26
+ readonly declineCode: string | undefined;
27
+ readonly requestId: string | undefined;
28
+ readonly status: number;
29
+ /**
30
+ * The `Idempotency-Key` the failed request was sent with — yours, or the one the client generated
31
+ * for a POST you did not give a key. `undefined` for a GET, which has none.
32
+ *
33
+ * 🔴 To retry a failed POST, pass THIS back: `{ idempotencyKey: err.idempotencyKey }`. Calling the
34
+ * method again without it sends a new key, and a new key is a new request to the API: if the
35
+ * first one did reach the bank, that is a second charge or refund.
36
+ *
37
+ * The one exception is `isRefundNotYetPossible`: the bank definitely refused and nothing moved,
38
+ * so the retry after settlement takes a NEW key — this one stays bound to the refusal for 24 hours
39
+ * and replays it. Calling the method again WITHOUT `idempotencyKey` already sends a fresh key.
40
+ */
41
+ readonly idempotencyKey: string | undefined;
42
+ /**
43
+ * On `403 permission_error` / `insufficient_permissions`: the permission this request needed and
44
+ * your API key — or, from a dashboard session, your role — does not have, such as
45
+ * `payments:refund`. Ask the account's owner for it; retrying will not help (`isRetryable` is
46
+ * false). `undefined` on every other error.
47
+ */
48
+ readonly requiredCapability: string | undefined;
49
+ /**
50
+ * `requestId` falls back to the `X-Request-Id` response header: a response that never reached
51
+ * the API's error handler (a proxy's 502, say) has no envelope to carry it, and the header is
52
+ * the one thing support can still look up.
53
+ */
54
+ constructor(status: number, body: ApiErrorBody, requestIdHeader?: string | null, idempotencyKey?: string);
55
+ /**
56
+ * True when retrying the identical request — with the SAME idempotency key — could reasonably
57
+ * succeed soon. For a POST that means `{ idempotencyKey: err.idempotencyKey }`, never a new key.
58
+ *
59
+ * `idempotency_in_progress` is included: the first request with that key is still running, and
60
+ * the API answers a retry either with another 409 or, once it has finished, with its stored
61
+ * answer, so a retry under that key cannot run the operation twice. The client already retries
62
+ * it for you, up to `maxRetries` times and never past 30 seconds from the first attempt.
63
+ * `idempotency_key_in_use` (a key reused with a different body) is not: that is a bug in the
64
+ * caller and will fail identically for ever.
65
+ *
66
+ * `payment_in_progress` is deliberately NOT retryable. See `isPaymentInProgress`. Nor is
67
+ * `outcome_unknown`, although it is a 500. See `isOutcomeUnknown`.
68
+ */
69
+ get isRetryable(): boolean;
70
+ /**
71
+ * `409 payment_in_progress`: the customer may be on the bank's page, or in 3-D Secure, for this
72
+ * payment right now, so it cannot be cancelled yet. Nothing was changed. The message says when to
73
+ * try again — minutes, not a backoff — and an unpaid payment is cancelled automatically anyway.
74
+ * The client never retries it: a loop here would only poll the bank on the customer's behalf.
75
+ */
76
+ get isPaymentInProgress(): boolean;
77
+ /**
78
+ * `400 refund_not_yet_possible`: a partial refund on a payment the bank has not settled yet. The
79
+ * bank refunds only settled payments and cannot reverse part of one. Nothing was refunded. Retry
80
+ * once it has settled — usually by the next business day — with a NEW idempotency key: this
81
+ * answer is stored against the old one for 24 hours and replayed. Calling the method again
82
+ * WITHOUT `idempotencyKey` already sends a fresh key. A new key is safe for this error only. If
83
+ * nothing has been refunded yet, a full refund can be made now.
84
+ */
85
+ get isRefundNotYetPossible(): boolean;
86
+ /**
87
+ * 🔴 `500 outcome_unknown`: the request asked the bank to move money — a charge, a capture, a
88
+ * refund — and the API could not confirm what happened. The money MAY HAVE MOVED.
89
+ *
90
+ * - Do not send it again under a NEW idempotency key: that is a second charge or refund.
91
+ * - Retrying under the SAME key (`err.idempotencyKey`) is always safe — it never reaches the bank
92
+ * again. Usually the API has stored this answer and replays it. If the message says an earlier
93
+ * request with the key "has not recorded its result yet", the original is still in flight, and
94
+ * a LATER retry under the same key returns its result once it finishes. Either way an immediate
95
+ * retry gains nothing, so the client does not retry it, and `isRetryable` is false.
96
+ * - Find out by reading the object: retrieve the payment intent (or list its refunds, or the
97
+ * invoice's payments) and look at its `status`. It settles on its own within minutes and its
98
+ * webhook follows.
99
+ */
100
+ get isOutcomeUnknown(): boolean;
101
+ }
102
+ /** The request never got an answer: a timeout, a DNS failure, a dropped connection. */
103
+ export declare class SecureProcessingConnectionError extends Error {
104
+ readonly cause: unknown;
105
+ /**
106
+ * 🔴 The `Idempotency-Key` the request was sent with, generated or yours. A connection failure
107
+ * does not say whether the request reached us: retry with `{ idempotencyKey: err.idempotencyKey }`
108
+ * and the API replays its answer if it did; a new key would run it again.
109
+ */
110
+ readonly idempotencyKey: string | undefined;
111
+ constructor(message: string, cause?: unknown, idempotencyKey?: string);
112
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,140 @@
1
+ // @ts-self-types="./errors.d.ts"
2
+ /**
3
+ * Something the API refused.
4
+ *
5
+ * Branch on `type` for the category and `code` for the specific reason. For a declined card,
6
+ * `declineCode` says why in terms you can show a customer. `requestId` is what to quote to support.
7
+ * `param` names the field at fault — including a `404 resource_missing` for an id in the body that
8
+ * is not yours (`param: "customer"`). A `403` names the missing permission in `requiredCapability`.
9
+ */
10
+ export class SecureProcessingError extends Error {
11
+ type;
12
+ code;
13
+ param;
14
+ declineCode;
15
+ requestId;
16
+ status;
17
+ /**
18
+ * The `Idempotency-Key` the failed request was sent with — yours, or the one the client generated
19
+ * for a POST you did not give a key. `undefined` for a GET, which has none.
20
+ *
21
+ * 🔴 To retry a failed POST, pass THIS back: `{ idempotencyKey: err.idempotencyKey }`. Calling the
22
+ * method again without it sends a new key, and a new key is a new request to the API: if the
23
+ * first one did reach the bank, that is a second charge or refund.
24
+ *
25
+ * The one exception is `isRefundNotYetPossible`: the bank definitely refused and nothing moved,
26
+ * so the retry after settlement takes a NEW key — this one stays bound to the refusal for 24 hours
27
+ * and replays it. Calling the method again WITHOUT `idempotencyKey` already sends a fresh key.
28
+ */
29
+ idempotencyKey;
30
+ /**
31
+ * On `403 permission_error` / `insufficient_permissions`: the permission this request needed and
32
+ * your API key — or, from a dashboard session, your role — does not have, such as
33
+ * `payments:refund`. Ask the account's owner for it; retrying will not help (`isRetryable` is
34
+ * false). `undefined` on every other error.
35
+ */
36
+ requiredCapability;
37
+ /**
38
+ * `requestId` falls back to the `X-Request-Id` response header: a response that never reached
39
+ * the API's error handler (a proxy's 502, say) has no envelope to carry it, and the header is
40
+ * the one thing support can still look up.
41
+ */
42
+ constructor(status, body, requestIdHeader, idempotencyKey) {
43
+ super(body.error.code === "outcome_unknown"
44
+ ? `${body.error.message} ${outcomeUnknownNote(idempotencyKey)}`
45
+ : body.error.message);
46
+ this.name = "SecureProcessingError";
47
+ this.idempotencyKey = idempotencyKey;
48
+ this.requiredCapability = body.error.required_capability;
49
+ this.status = status;
50
+ this.type = body.error.type;
51
+ this.code = body.error.code;
52
+ this.param = body.error.param;
53
+ this.declineCode = body.error.decline_code;
54
+ this.requestId = body.error.request_id ?? requestIdHeader ?? undefined;
55
+ }
56
+ /**
57
+ * True when retrying the identical request — with the SAME idempotency key — could reasonably
58
+ * succeed soon. For a POST that means `{ idempotencyKey: err.idempotencyKey }`, never a new key.
59
+ *
60
+ * `idempotency_in_progress` is included: the first request with that key is still running, and
61
+ * the API answers a retry either with another 409 or, once it has finished, with its stored
62
+ * answer, so a retry under that key cannot run the operation twice. The client already retries
63
+ * it for you, up to `maxRetries` times and never past 30 seconds from the first attempt.
64
+ * `idempotency_key_in_use` (a key reused with a different body) is not: that is a bug in the
65
+ * caller and will fail identically for ever.
66
+ *
67
+ * `payment_in_progress` is deliberately NOT retryable. See `isPaymentInProgress`. Nor is
68
+ * `outcome_unknown`, although it is a 500. See `isOutcomeUnknown`.
69
+ */
70
+ get isRetryable() {
71
+ if (this.isOutcomeUnknown)
72
+ return false;
73
+ return (this.status >= 500 ||
74
+ this.status === 429 ||
75
+ this.code === "gateway_unavailable" ||
76
+ this.code === "idempotency_in_progress");
77
+ }
78
+ /**
79
+ * `409 payment_in_progress`: the customer may be on the bank's page, or in 3-D Secure, for this
80
+ * payment right now, so it cannot be cancelled yet. Nothing was changed. The message says when to
81
+ * try again — minutes, not a backoff — and an unpaid payment is cancelled automatically anyway.
82
+ * The client never retries it: a loop here would only poll the bank on the customer's behalf.
83
+ */
84
+ get isPaymentInProgress() {
85
+ return this.code === "payment_in_progress";
86
+ }
87
+ /**
88
+ * `400 refund_not_yet_possible`: a partial refund on a payment the bank has not settled yet. The
89
+ * bank refunds only settled payments and cannot reverse part of one. Nothing was refunded. Retry
90
+ * once it has settled — usually by the next business day — with a NEW idempotency key: this
91
+ * answer is stored against the old one for 24 hours and replayed. Calling the method again
92
+ * WITHOUT `idempotencyKey` already sends a fresh key. A new key is safe for this error only. If
93
+ * nothing has been refunded yet, a full refund can be made now.
94
+ */
95
+ get isRefundNotYetPossible() {
96
+ return this.code === "refund_not_yet_possible";
97
+ }
98
+ /**
99
+ * 🔴 `500 outcome_unknown`: the request asked the bank to move money — a charge, a capture, a
100
+ * refund — and the API could not confirm what happened. The money MAY HAVE MOVED.
101
+ *
102
+ * - Do not send it again under a NEW idempotency key: that is a second charge or refund.
103
+ * - Retrying under the SAME key (`err.idempotencyKey`) is always safe — it never reaches the bank
104
+ * again. Usually the API has stored this answer and replays it. If the message says an earlier
105
+ * request with the key "has not recorded its result yet", the original is still in flight, and
106
+ * a LATER retry under the same key returns its result once it finishes. Either way an immediate
107
+ * retry gains nothing, so the client does not retry it, and `isRetryable` is false.
108
+ * - Find out by reading the object: retrieve the payment intent (or list its refunds, or the
109
+ * invoice's payments) and look at its `status`. It settles on its own within minutes and its
110
+ * webhook follows.
111
+ */
112
+ get isOutcomeUnknown() {
113
+ return this.code === "outcome_unknown";
114
+ }
115
+ }
116
+ /** Appended to an `outcome_unknown` message so the instruction travels into whatever logs it. */
117
+ function outcomeUnknownNote(idempotencyKey) {
118
+ const key = idempotencyKey ? ` (it was sent with Idempotency-Key ${idempotencyKey}, err.idempotencyKey)` : "";
119
+ return (`(Do not retry this under a new Idempotency-Key${key} — the money may already have moved. ` +
120
+ "Retrieve the payment, refund or invoice and check its status; it settles on its own and a " +
121
+ "webhook follows.)");
122
+ }
123
+ /** The request never got an answer: a timeout, a DNS failure, a dropped connection. */
124
+ export class SecureProcessingConnectionError extends Error {
125
+ // `override` because Error already declares `cause`; keeping the original is what lets a caller
126
+ // see the underlying network failure rather than only our wrapper.
127
+ cause;
128
+ /**
129
+ * 🔴 The `Idempotency-Key` the request was sent with, generated or yours. A connection failure
130
+ * does not say whether the request reached us: retry with `{ idempotencyKey: err.idempotencyKey }`
131
+ * and the API replays its answer if it did; a new key would run it again.
132
+ */
133
+ idempotencyKey;
134
+ constructor(message, cause, idempotencyKey) {
135
+ super(message);
136
+ this.name = "SecureProcessingConnectionError";
137
+ this.cause = cause;
138
+ this.idempotencyKey = idempotencyKey;
139
+ }
140
+ }