@peable.to/shared-types 0.10.0 → 0.12.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.
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CATALOG_LIMITS = exports.COUPON_DURATIONS = exports.TRANSFORM_QUANTITY_ROUNDS = exports.PRICE_TAX_BEHAVIORS = exports.RECURRING_USAGE_TYPES = exports.RECURRING_INTERVALS = exports.PRICE_TYPES = exports.PRICE_TIERS_MODES = exports.PRICE_BILLING_SCHEMES = void 0;
4
+ /** How a price's amount is computed from a quantity. */
5
+ exports.PRICE_BILLING_SCHEMES = ['per_unit', 'tiered'];
6
+ /**
7
+ * How a tiered price reads its tiers.
8
+ *
9
+ * - `volume` — the WHOLE quantity is priced at the tier it falls into.
10
+ * - `graduated` — each unit is priced at the tier it falls into, so a
11
+ * quantity crossing two tiers pays each tier's rate for its
12
+ * share, plus the flat amount of every tier it reaches.
13
+ */
14
+ exports.PRICE_TIERS_MODES = ['graduated', 'volume'];
15
+ /** `recurring` when the price has a recurring component, `one_time` otherwise. */
16
+ exports.PRICE_TYPES = ['one_time', 'recurring'];
17
+ exports.RECURRING_INTERVALS = ['day', 'week', 'month', 'year'];
18
+ /**
19
+ * `licensed` bills the quantity on the subscription. `metered` bills reported
20
+ * usage and needs a meter; meters are not part of Orbit yet, so the gateway
21
+ * refuses a metered price rather than store one nothing can bill.
22
+ */
23
+ exports.RECURRING_USAGE_TYPES = ['licensed', 'metered'];
24
+ /**
25
+ * Whether a price includes tax (Peable Tax, ADR 0011). `unspecified` defers to
26
+ * the merchant's default tax behavior; once a price says `inclusive` or
27
+ * `exclusive` it can never change.
28
+ */
29
+ exports.PRICE_TAX_BEHAVIORS = ['inclusive', 'exclusive', 'unspecified'];
30
+ exports.TRANSFORM_QUANTITY_ROUNDS = ['up', 'down'];
31
+ /** How long a coupon's discount lasts once applied to a subscription. */
32
+ exports.COUPON_DURATIONS = ['once', 'repeating', 'forever'];
33
+ /** The bounds the gateway enforces, published so a form can say them first. */
34
+ exports.CATALOG_LIMITS = Object.freeze({
35
+ productNameMax: 250,
36
+ productDescriptionMax: 40000,
37
+ productImagesMax: 8,
38
+ urlMax: 2048,
39
+ unitLabelMax: 12,
40
+ statementDescriptorMax: 22,
41
+ marketingFeaturesMax: 15,
42
+ marketingFeatureNameMax: 80,
43
+ priceNicknameMax: 250,
44
+ lookupKeyMax: 200,
45
+ lookupKeysFilterMax: 10,
46
+ priceTiersMax: 250,
47
+ couponIdMax: 64,
48
+ couponNameMax: 40,
49
+ couponAppliesToProductsMax: 100,
50
+ couponDurationInMonthsMax: 1200,
51
+ promotionCodeMax: 64,
52
+ idsFilterMax: 100,
53
+ });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/cjs/event.js CHANGED
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PEABLE_API_VERSION = void 0;
4
+ /**
5
+ * The wire date every response names in `Peable-Version`, and the
6
+ * `api_version` an event is stored under (ADR 0014 D2/D4). One constant, so
7
+ * the header and the stored events cannot name two different wires.
8
+ */
9
+ exports.PEABLE_API_VERSION = '2026-11-03';
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TAX_LOCATION_EVIDENCE_TYPES = exports.TAX_REGISTRATION_SCHEMES = exports.PEABLE_BILLING_AUTHORITY_KEYS = exports.canonicalBillingTaxQuote = exports.canonicalBillingAuthority = exports.BILLING_SUBSCRIPTION_STATUSES = exports.SOCIAL_SOURCE_REF_MAX_LENGTH = exports.SOCIAL_SOURCE_APP_MAX_LENGTH = exports.MERCHANT_ENVIRONMENTS = exports.verifyWebhook = exports.signWebhook = exports.canExpire = exports.EXPIRABLE_STATUSES = exports.CARD_ONLY_STATUSES = exports.CHAIN_ONLY_STATUSES = exports.PAYMENT_INTENT_RAILS = exports.PAYMENT_INTENT_STATUSES = exports.POST_SETTLEMENT_STATUSES = exports.reachesSettlementFromPayer = exports.canStillBePaid = exports.isValidStatusTransition = exports.isCurrencyCode = exports.decimalsFor = exports.CURRENCY_DECIMALS = exports.CURRENCY_CODES = exports.isBaseUnitString = exports.UNITS_PER_COIN = void 0;
3
+ exports.TAX_LOCATION_EVIDENCE_TYPES = exports.TAX_REGISTRATION_SCHEMES = exports.PEABLE_BILLING_AUTHORITY_KEYS = exports.canonicalBillingTaxQuote = exports.canonicalBillingAuthority = exports.BILLING_SUBSCRIPTION_STATUSES = exports.SOCIAL_SOURCE_REF_MAX_LENGTH = exports.SOCIAL_SOURCE_APP_MAX_LENGTH = exports.MERCHANT_ENVIRONMENTS = exports.verifyWebhook = exports.signWebhook = exports.CATALOG_LIMITS = exports.COUPON_DURATIONS = exports.TRANSFORM_QUANTITY_ROUNDS = exports.PRICE_TAX_BEHAVIORS = exports.RECURRING_USAGE_TYPES = exports.RECURRING_INTERVALS = exports.PRICE_TYPES = exports.PRICE_TIERS_MODES = exports.PRICE_BILLING_SCHEMES = exports.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS = exports.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = exports.WEBHOOK_ENDPOINT_DISABLED_REASONS = exports.WEBHOOK_ENDPOINT_STATUSES = exports.PEABLE_API_VERSION = exports.SETUP_INTENT_STATUSES = exports.isValidStatusTransition = exports.canStillBePaid = exports.canExpire = exports.TERMINAL_STATUSES = exports.PAYMENT_INTENT_STATUSES = exports.PAYMENT_INTENT_SETUP_FUTURE_USAGES = exports.PAYMENT_INTENT_RAILS = exports.PAYMENT_INTENT_CAPTURE_METHODS = exports.PAYMENT_INTENT_CANCELLATION_REASONS = exports.MERCHANT_CANCELLATION_REASONS = exports.EXPIRABLE_STATUSES = exports.CARD_ONLY_STATUSES = exports.isCurrencyCode = exports.decimalsFor = exports.CURRENCY_DECIMALS = exports.CURRENCY_CODES = exports.isBaseUnitString = exports.UNITS_PER_COIN = void 0;
4
4
  // @peable.to/shared-types — public API for the Peable Gateway contract.
5
5
  var money_1 = require("./money");
6
6
  Object.defineProperty(exports, "UNITS_PER_COIN", { enumerable: true, get: function () { return money_1.UNITS_PER_COIN; } });
@@ -10,16 +10,37 @@ Object.defineProperty(exports, "CURRENCY_DECIMALS", { enumerable: true, get: fun
10
10
  Object.defineProperty(exports, "decimalsFor", { enumerable: true, get: function () { return money_1.decimalsFor; } });
11
11
  Object.defineProperty(exports, "isCurrencyCode", { enumerable: true, get: function () { return money_1.isCurrencyCode; } });
12
12
  var paymentIntent_1 = require("./paymentIntent");
13
- Object.defineProperty(exports, "isValidStatusTransition", { enumerable: true, get: function () { return paymentIntent_1.isValidStatusTransition; } });
14
- Object.defineProperty(exports, "canStillBePaid", { enumerable: true, get: function () { return paymentIntent_1.canStillBePaid; } });
15
- Object.defineProperty(exports, "reachesSettlementFromPayer", { enumerable: true, get: function () { return paymentIntent_1.reachesSettlementFromPayer; } });
16
- Object.defineProperty(exports, "POST_SETTLEMENT_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.POST_SETTLEMENT_STATUSES; } });
17
- Object.defineProperty(exports, "PAYMENT_INTENT_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_STATUSES; } });
18
- Object.defineProperty(exports, "PAYMENT_INTENT_RAILS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_RAILS; } });
19
- Object.defineProperty(exports, "CHAIN_ONLY_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.CHAIN_ONLY_STATUSES; } });
20
13
  Object.defineProperty(exports, "CARD_ONLY_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.CARD_ONLY_STATUSES; } });
21
14
  Object.defineProperty(exports, "EXPIRABLE_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.EXPIRABLE_STATUSES; } });
15
+ Object.defineProperty(exports, "MERCHANT_CANCELLATION_REASONS", { enumerable: true, get: function () { return paymentIntent_1.MERCHANT_CANCELLATION_REASONS; } });
16
+ Object.defineProperty(exports, "PAYMENT_INTENT_CANCELLATION_REASONS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_CANCELLATION_REASONS; } });
17
+ Object.defineProperty(exports, "PAYMENT_INTENT_CAPTURE_METHODS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_CAPTURE_METHODS; } });
18
+ Object.defineProperty(exports, "PAYMENT_INTENT_RAILS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_RAILS; } });
19
+ Object.defineProperty(exports, "PAYMENT_INTENT_SETUP_FUTURE_USAGES", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_SETUP_FUTURE_USAGES; } });
20
+ Object.defineProperty(exports, "PAYMENT_INTENT_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_STATUSES; } });
21
+ Object.defineProperty(exports, "TERMINAL_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.TERMINAL_STATUSES; } });
22
22
  Object.defineProperty(exports, "canExpire", { enumerable: true, get: function () { return paymentIntent_1.canExpire; } });
23
+ Object.defineProperty(exports, "canStillBePaid", { enumerable: true, get: function () { return paymentIntent_1.canStillBePaid; } });
24
+ Object.defineProperty(exports, "isValidStatusTransition", { enumerable: true, get: function () { return paymentIntent_1.isValidStatusTransition; } });
25
+ var setupIntent_1 = require("./setupIntent");
26
+ Object.defineProperty(exports, "SETUP_INTENT_STATUSES", { enumerable: true, get: function () { return setupIntent_1.SETUP_INTENT_STATUSES; } });
27
+ var event_1 = require("./event");
28
+ Object.defineProperty(exports, "PEABLE_API_VERSION", { enumerable: true, get: function () { return event_1.PEABLE_API_VERSION; } });
29
+ var webhookEndpoint_1 = require("./webhookEndpoint");
30
+ Object.defineProperty(exports, "WEBHOOK_ENDPOINT_STATUSES", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_ENDPOINT_STATUSES; } });
31
+ Object.defineProperty(exports, "WEBHOOK_ENDPOINT_DISABLED_REASONS", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_ENDPOINT_DISABLED_REASONS; } });
32
+ Object.defineProperty(exports, "WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT; } });
33
+ Object.defineProperty(exports, "WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS; } });
34
+ var catalog_1 = require("./catalog");
35
+ Object.defineProperty(exports, "PRICE_BILLING_SCHEMES", { enumerable: true, get: function () { return catalog_1.PRICE_BILLING_SCHEMES; } });
36
+ Object.defineProperty(exports, "PRICE_TIERS_MODES", { enumerable: true, get: function () { return catalog_1.PRICE_TIERS_MODES; } });
37
+ Object.defineProperty(exports, "PRICE_TYPES", { enumerable: true, get: function () { return catalog_1.PRICE_TYPES; } });
38
+ Object.defineProperty(exports, "RECURRING_INTERVALS", { enumerable: true, get: function () { return catalog_1.RECURRING_INTERVALS; } });
39
+ Object.defineProperty(exports, "RECURRING_USAGE_TYPES", { enumerable: true, get: function () { return catalog_1.RECURRING_USAGE_TYPES; } });
40
+ Object.defineProperty(exports, "PRICE_TAX_BEHAVIORS", { enumerable: true, get: function () { return catalog_1.PRICE_TAX_BEHAVIORS; } });
41
+ Object.defineProperty(exports, "TRANSFORM_QUANTITY_ROUNDS", { enumerable: true, get: function () { return catalog_1.TRANSFORM_QUANTITY_ROUNDS; } });
42
+ Object.defineProperty(exports, "COUPON_DURATIONS", { enumerable: true, get: function () { return catalog_1.COUPON_DURATIONS; } });
43
+ Object.defineProperty(exports, "CATALOG_LIMITS", { enumerable: true, get: function () { return catalog_1.CATALOG_LIMITS; } });
23
44
  var webhookSigner_1 = require("./webhookSigner");
24
45
  Object.defineProperty(exports, "signWebhook", { enumerable: true, get: function () { return webhookSigner_1.signWebhook; } });
25
46
  Object.defineProperty(exports, "verifyWebhook", { enumerable: true, get: function () { return webhookSigner_1.verifyWebhook; } });
@@ -1,231 +1,133 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EXPIRABLE_STATUSES = exports.POST_SETTLEMENT_STATUSES = exports.PAYMENT_INTENT_STATUSES = exports.CARD_ONLY_STATUSES = exports.CHAIN_ONLY_STATUSES = exports.PAYMENT_INTENT_RAILS = void 0;
3
+ exports.PAYMENT_INTENT_SETUP_FUTURE_USAGES = exports.PAYMENT_INTENT_CAPTURE_METHODS = exports.MERCHANT_CANCELLATION_REASONS = exports.PAYMENT_INTENT_CANCELLATION_REASONS = exports.EXPIRABLE_STATUSES = exports.TERMINAL_STATUSES = exports.PAYMENT_INTENT_STATUSES = exports.CARD_ONLY_STATUSES = exports.PAYMENT_INTENT_RAILS = void 0;
4
4
  exports.isValidStatusTransition = isValidStatusTransition;
5
5
  exports.canStillBePaid = canStillBePaid;
6
- exports.reachesSettlementFromPayer = reachesSettlementFromPayer;
7
6
  exports.canExpire = canExpire;
8
7
  /** {@link PaymentIntentRail} as the tuple the database CHECK reads. */
9
8
  exports.PAYMENT_INTENT_RAILS = ['faircoin', 'card'];
10
9
  /**
11
- * Which statuses belong to which rail (ADR 0001 D5).
10
+ * The statuses only a CARD payment can reach (ADR 0019).
12
11
  *
13
- * The four chain states describe a transaction on a blockchain and the four
14
- * card states describe an authorization at a card acquirer; neither set is
15
- * expressible on the other rail. Mapping a card authorization onto `broadcast`
16
- * would be cheaper and would be a lie — nothing was broadcast, there is no
17
- * transaction, and `payment_intents_broadcast_requires_txid_check` would then
18
- * be demanding a txid from a payment that can never have one.
19
- *
20
- * `db/schema/payments.ts` renders these two sets into CHECK constraints, so the
21
- * restriction holds against a write that skipped the application.
12
+ * A FairCoin payment has no payment method to require (the payer's wallet signs
13
+ * on their own device), nothing to confirm server-side, and no authorization
14
+ * to capture — coins either arrive or they do not. `db/schema/payments.ts`
15
+ * renders this set into a CHECK, so the restriction holds against a write that
16
+ * skipped the application.
22
17
  */
23
- exports.CHAIN_ONLY_STATUSES = [
24
- 'awaiting_approval',
25
- 'approved',
26
- 'broadcast',
27
- 'confirming',
28
- ];
29
18
  exports.CARD_ONLY_STATUSES = [
30
- 'requires_action',
31
- 'processing',
32
- 'refunded',
33
- 'partially_refunded',
19
+ 'requires_payment_method',
20
+ 'requires_confirmation',
21
+ 'requires_capture',
34
22
  ];
35
23
  /**
36
- * Legal forward transitions of the PaymentIntent lifecycle. Terminal states
37
- * (refunded/expired/failed/rejected) allow no further transition here — the one
38
- * documented exception, a reorg dropping `settled` back to `confirming`, is
39
- * modelled by the backend state machine, not this happy-path table.
24
+ * Legal transitions of the PaymentIntent lifecycle.
40
25
  *
41
- * ONE table for both rails, deliberately. It answers "is this a legal lifecycle
42
- * edge at all"; WHICH rail may reach a given status is the separate, structural
43
- * question the two sets above answer, in the database. A rail-parameterised
44
- * table would put the same restriction in two places and let them disagree.
26
+ * ONE table for both rails. It answers "is this a legal lifecycle edge at all";
27
+ * WHICH rail may reach a given status is the separate, structural question
28
+ * {@link CARD_ONLY_STATUSES} answers in the database, and which EVENT may walk
29
+ * an edge is `LEGAL_SOURCES` in the backend's `services/intentState.ts`.
45
30
  *
46
31
  * Self-transitions are absent because `applyEvent` short-circuits on
47
- * `current === target` before consulting this table — that is the idempotent
48
- * re-poll of the settlement watcher, and a second partial refund, which change
49
- * an amount without changing a status.
32
+ * `current === target` — the idempotent re-poll of the settlement watcher.
33
+ *
34
+ * `succeeded` has no outgoing edge here. Its one exception — a chain reorg
35
+ * dropping a FairCoin payment back below its confirmation threshold,
36
+ * `succeeded → processing` — is modelled by the backend state machine, not this
37
+ * table. Refunds are not statuses: money coming back is `amount_refunded` and
38
+ * the refunds themselves (ADR 0019).
50
39
  */
51
40
  const ALLOWED = {
52
- // `created` supports three paths: the rich wallet flow (→ awaiting_approval →
53
- // approved → broadcast), the minimal path where the payer pays out-of-band
54
- // and reports the txid directly (→ broadcast), and the card path (→
55
- // requires_action for an SCA challenge, → processing, or straight to settled
56
- // for a charge that confirms in one call). `failed` is reachable from here
57
- // because a card can be declined on first confirmation, having never been
58
- // anything else; no FairCoin path uses that edge, since `underpaid` is only
59
- // emitted for an intent that already carries a txid.
60
- created: [
61
- 'awaiting_approval',
62
- 'broadcast',
41
+ requires_payment_method: [
42
+ 'requires_confirmation',
43
+ 'requires_action',
44
+ 'processing',
45
+ 'requires_capture',
46
+ 'succeeded',
47
+ 'canceled',
48
+ ],
49
+ requires_confirmation: [
50
+ 'requires_payment_method',
63
51
  'requires_action',
64
52
  'processing',
65
- 'settled',
66
- 'failed',
67
- 'rejected',
68
- 'expired',
53
+ 'requires_capture',
54
+ 'succeeded',
55
+ 'canceled',
69
56
  ],
70
- awaiting_approval: ['approved', 'rejected', 'expired'],
71
- approved: ['broadcast', 'failed'],
72
- broadcast: ['confirming', 'failed'],
73
- confirming: ['settled', 'failed'],
74
- requires_action: ['processing', 'settled', 'failed', 'expired', 'rejected'],
75
- processing: ['settled', 'failed'],
76
- /**
77
- * A failed ATTEMPT is not a failed payment, on the card rail.
78
- *
79
- * `payment_intent.payment_failed` means one authorization attempt was
80
- * declined; Stripe returns the PaymentIntent to `requires_payment_method` and
81
- * the payer can confirm it again with another card. With `failed` terminal,
82
- * the later `payment_intent.succeeded` was an ILLEGAL transition — so the
83
- * drain recorded a processing failure and retried it forever, the payment
84
- * stayed `failed`, and a merchant who had been paid was told their payment
85
- * had been declined.
86
- *
87
- * On the chain rail `failed` means `underpaid`, which IS terminal — coins
88
- * arrived and were not enough. Nothing here opens that door: `underpaid` is
89
- * emitted only by the settlement watcher, and `LEGAL_SOURCES` in
90
- * `services/intentState.ts` keeps every `card_*` event to card statuses and
91
- * the chain's `confirmed` to `confirming`. This table answers "is the edge
92
- * legal at all"; which rail may walk it is the separate question those two
93
- * guards answer.
94
- */
95
- failed: ['requires_action', 'processing', 'settled'],
96
- // Money coming back requires money to have arrived, so both refund states are
97
- // reachable only from `settled`.
98
- settled: ['refunded', 'partially_refunded'],
99
- /**
100
- * ...and money coming back can be UNDONE, which is why these edges run both
101
- * ways.
102
- *
103
- * A refund is not final when the provider accepts it: a bank can reject one
104
- * days later, and the provider then says so (`refund.failed`). Without a path
105
- * back, a payment whose only refund failed would claim `refunded` forever —
106
- * the merchant's books saying money went back that is still with them, and no
107
- * transition anywhere able to correct it.
108
- *
109
- * The target is always recomputed from the SUM of succeeded refunds
110
- * (`applyRefundToIntent`), never stepped, so these edges describe where that
111
- * sum can legally land rather than a sequence anyone walks.
112
- */
113
- partially_refunded: ['refunded', 'settled'],
114
- refunded: ['partially_refunded', 'settled'],
115
- expired: [],
116
- rejected: [],
57
+ // FairCoin: the payer broadcasts (→ processing), or the intent is cancelled
58
+ // or expires (→ canceled). Card: an SCA challenge resolved either way.
59
+ requires_action: [
60
+ 'requires_payment_method',
61
+ 'processing',
62
+ 'requires_capture',
63
+ 'succeeded',
64
+ 'canceled',
65
+ ],
66
+ // FairCoin: confirmed (→ succeeded) or underpaid (→ canceled). Card: an async
67
+ // method settled, was authorized, or failed back to `requires_payment_method`.
68
+ processing: ['requires_payment_method', 'requires_capture', 'succeeded', 'canceled'],
69
+ requires_capture: ['processing', 'succeeded', 'canceled'],
70
+ succeeded: [],
71
+ canceled: [],
117
72
  };
118
73
  function isValidStatusTransition(from, to) {
119
74
  return ALLOWED[from].includes(to);
120
75
  }
121
- /** Every legal `PaymentIntentStatus`, derived from `ALLOWED`'s keys — the
122
- * single source of truth consumed by the Gateway's list-route status filter. */
76
+ /** Every `PaymentIntentStatus`, derived from `ALLOWED`'s keys. */
123
77
  exports.PAYMENT_INTENT_STATUSES = Object.keys(ALLOWED);
124
- /**
125
- * The statuses a payer can still complete a payment FROM.
126
- *
127
- * **Twice now this question has been answered by walking the transition table,
128
- * and twice the table has moved underneath it.**
129
- *
130
- * The hosted checkout first asked leaf-ness — "has this status no outgoing
131
- * edges" — which meant `settled` while `settled` had none. Adding the refund
132
- * transitions (`settled → refunded | partially_refunded`) gave it edges, so a
133
- * settled payment stopped looking finished, and the checkout began REUSING a
134
- * remembered settled intent instead of minting a fresh one: a payer who had
135
- * already paid saw their old receipt forever, with no way to pay the link
136
- * again.
137
- *
138
- * The fix was reachability — "is `settled` still reachable" — and that held
139
- * until refunds could be UNDONE. `refunded → settled` (a bank rejecting a
140
- * refund days later) makes `settled` reachable from `settled` in two steps, so
141
- * reachability answers `true` for a payment that is finished, and the original
142
- * bug returns unchanged.
143
- *
144
- * So the set is stated, not derived. It is exactly the statuses in which the
145
- * gateway is still waiting on the PAYER — which is what the question means, in
146
- * the payer's own terms, and is the one formulation no edge added elsewhere can
147
- * silently change. `paymentIntent.test.ts` pins it against the table: every
148
- * member must still be able to reach `settled` without passing through a
149
- * post-settlement status, and no non-member may.
150
- */
151
- const PAYABLE_STATUSES = new Set([
152
- 'created',
153
- 'awaiting_approval',
154
- 'approved',
155
- 'broadcast',
156
- 'confirming',
157
- 'requires_action',
158
- 'processing',
159
- ]);
78
+ /** The statuses that end a payment: nothing leaves them (bar the reorg exception). */
79
+ exports.TERMINAL_STATUSES = ['succeeded', 'canceled'];
160
80
  /**
161
81
  * Whether a payer can still complete a payment against an intent in this
162
- * status.
163
- *
164
- * `settled` answers `false`: it is where paying ENDS. `partially_refunded`
165
- * answers `false` too — money can still move, but not from the payer.
82
+ * status — every non-terminal one. Stated, not derived from reachability:
83
+ * `paymentIntent.test.ts` pins it against the table.
166
84
  */
167
85
  function canStillBePaid(status) {
168
- return PAYABLE_STATUSES.has(status);
169
- }
170
- /**
171
- * The statuses a payment reaches only AFTER the payer has finished.
172
- *
173
- * Exported for the gate below rather than for production use: the property that
174
- * makes `PAYABLE_STATUSES` checkable is that a payable status reaches `settled`
175
- * without going through one of these.
176
- */
177
- exports.POST_SETTLEMENT_STATUSES = [
178
- 'settled',
179
- 'refunded',
180
- 'partially_refunded',
181
- ];
182
- /**
183
- * Whether `settled` is reachable from `status` without passing through a
184
- * post-settlement status. The derivation `canStillBePaid` used to BE, kept as
185
- * the thing that checks it rather than as the thing that answers it.
186
- */
187
- function reachesSettlementFromPayer(status) {
188
- const blocked = new Set(exports.POST_SETTLEMENT_STATUSES);
189
- // A payment that has ALREADY settled is one the payer has finished with,
190
- // whatever edges lead out of it. Checked before the walk rather than inside
191
- // it, because `partially_refunded → settled` is a direct edge and the walk
192
- // would find it in one step — and that edge is a refund being undone, not a
193
- // payer paying.
194
- if (blocked.has(status))
195
- return false;
196
- const seen = new Set();
197
- const queue = [...ALLOWED[status]];
198
- while (queue.length > 0) {
199
- const next = queue.shift();
200
- if (next === 'settled')
201
- return true;
202
- if (seen.has(next) || blocked.has(next))
203
- continue;
204
- seen.add(next);
205
- queue.push(...ALLOWED[next]);
206
- }
207
- return false;
86
+ return !exports.TERMINAL_STATUSES.includes(status);
208
87
  }
209
88
  /**
210
89
  * The statuses an intent may still EXPIRE from (ADR 0012 D9).
211
90
  *
212
- * Exactly those whose transition list contains `expired`: the gateway is
213
- * waiting on a PERSON who has not paid. Once a FairCoin payer has reported a
214
- * broadcast (`broadcast`), or the chain is confirming it (`confirming`), the
215
- * intent NEVER expires, whatever `expiresAt` says — coins are moving and the
216
- * settlement watcher owns the row. A platform must therefore never cancel an
217
- * order because `expiresAt` passed: it releases a reservation on
218
- * `payment_intent.expired` (or `rejected`), and on nothing else.
91
+ * The gateway is waiting on a PERSON who has not paid: a FairCoin payer who has
92
+ * not broadcast (`requires_action`), or a card payer who has not completed a
93
+ * payment method or a challenge. Once a FairCoin payer has reported a
94
+ * transaction (`processing`) the intent NEVER expires, whatever `expires_at`
95
+ * says — coins are moving and the settlement watcher owns the row. An
96
+ * authorization held for capture (`requires_capture`) is the merchant's to
97
+ * capture or cancel and does not expire either.
219
98
  *
220
- * Stated rather than derived, for `PAYABLE_STATUSES`' reason; the gateway's
221
- * test suite pins it against the transition table.
99
+ * Expiry is a cancellation with `cancellation_reason: 'abandoned'`: a platform
100
+ * releases a reservation on `payment_intent.canceled`.
222
101
  */
223
102
  exports.EXPIRABLE_STATUSES = [
224
- 'created',
225
- 'awaiting_approval',
103
+ 'requires_payment_method',
104
+ 'requires_confirmation',
226
105
  'requires_action',
227
106
  ];
228
107
  /** Whether an intent in this status can still expire. See {@link EXPIRABLE_STATUSES}. */
229
108
  function canExpire(status) {
230
109
  return exports.EXPIRABLE_STATUSES.includes(status);
231
110
  }
111
+ exports.PAYMENT_INTENT_CANCELLATION_REASONS = [
112
+ 'abandoned',
113
+ 'automatic',
114
+ 'duplicate',
115
+ 'fraudulent',
116
+ 'requested_by_customer',
117
+ 'underpaid',
118
+ ];
119
+ /** The reasons a MERCHANT may give `POST /v1/payment_intents/:id/cancel`. */
120
+ exports.MERCHANT_CANCELLATION_REASONS = [
121
+ 'abandoned',
122
+ 'duplicate',
123
+ 'fraudulent',
124
+ 'requested_by_customer',
125
+ ];
126
+ exports.PAYMENT_INTENT_CAPTURE_METHODS = [
127
+ 'automatic',
128
+ 'manual',
129
+ ];
130
+ exports.PAYMENT_INTENT_SETUP_FUTURE_USAGES = [
131
+ 'on_session',
132
+ 'off_session',
133
+ ];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SETUP_INTENT_STATUSES = void 0;
4
+ exports.SETUP_INTENT_STATUSES = [
5
+ 'requires_payment_method',
6
+ 'requires_confirmation',
7
+ 'requires_action',
8
+ 'processing',
9
+ 'canceled',
10
+ 'succeeded',
11
+ ];
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS = exports.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = exports.WEBHOOK_ENDPOINT_DISABLED_REASONS = exports.WEBHOOK_ENDPOINT_STATUSES = void 0;
4
+ exports.WEBHOOK_ENDPOINT_STATUSES = ['enabled', 'disabled'];
5
+ /**
6
+ * Why an endpoint is `disabled`.
7
+ *
8
+ * - `disabled_by_merchant` — its owner set `disabled: true`.
9
+ * - `delivery_failures` — every delivery to it failed for three days
10
+ * straight, so Peable stopped sending. Re-enabling it (`disabled: false`)
11
+ * starts the clock again.
12
+ */
13
+ exports.WEBHOOK_ENDPOINT_DISABLED_REASONS = [
14
+ 'disabled_by_merchant',
15
+ 'delivery_failures',
16
+ ];
17
+ /** At most this many endpoints per merchant (per mode). */
18
+ exports.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = 16;
19
+ /** The longest a rolled secret may keep signing beside its replacement. */
20
+ exports.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS = 24 * 60 * 60;
@@ -13,32 +13,40 @@ function computeSignature(secret, rawBody, timestamp) {
13
13
  }
14
14
  /**
15
15
  * Produce a Stripe-style signature header for an outbound webhook:
16
- * `t=${timestamp},v1=${hexHmacSha256}`.
16
+ * `t=${timestamp},v1=${hex}` — with one `v1=` per secret.
17
+ *
18
+ * Several secrets are what make a rotation gapless: while an endpoint's rolled
19
+ * secret is still inside its overlap window, every delivery is signed with
20
+ * BOTH, so a receiver verifying with either one accepts it.
17
21
  */
18
22
  function signWebhook(secret, rawBody, timestamp) {
19
- return `t=${timestamp},v1=${computeSignature(secret, rawBody, timestamp)}`;
23
+ const secrets = typeof secret === 'string' ? [secret] : secret;
24
+ if (secrets.length === 0)
25
+ throw new Error('signWebhook needs at least one secret');
26
+ const signatures = secrets.map((s) => `v1=${computeSignature(s, rawBody, timestamp)}`);
27
+ return `t=${timestamp},${signatures.join(',')}`;
20
28
  }
21
29
  function parseHeader(header) {
22
30
  let timestamp = null;
23
- let signature = null;
31
+ const signatures = [];
24
32
  for (const part of header.split(',')) {
25
33
  const separator = part.indexOf('=');
26
34
  if (separator === -1)
27
35
  continue;
28
- const key = part.slice(0, separator);
29
- const value = part.slice(separator + 1);
36
+ const key = part.slice(0, separator).trim();
37
+ const value = part.slice(separator + 1).trim();
30
38
  if (key === 't') {
31
39
  const parsed = Number(value);
32
40
  if (Number.isInteger(parsed))
33
41
  timestamp = parsed;
34
42
  }
35
43
  else if (key === 'v1') {
36
- signature = value;
44
+ signatures.push(value);
37
45
  }
38
46
  }
39
- if (timestamp === null || signature === null)
47
+ if (timestamp === null || signatures.length === 0)
40
48
  return null;
41
- return { timestamp, signature };
49
+ return { timestamp, signatures };
42
50
  }
43
51
  /**
44
52
  * Constant-time hex-string comparison. Returns false for unequal-length inputs
@@ -53,9 +61,13 @@ function constantTimeEqualHex(a, b) {
53
61
  }
54
62
  /**
55
63
  * Verify a webhook signature header against the raw body.
56
- * - Parses `t` and `v1` from the header.
64
+ *
65
+ * - Parses `t` and EVERY `v1` from the header.
57
66
  * - Rejects if the timestamp drifts more than `toleranceSec` from `nowSec`.
58
- * - Recomputes the expected signature and compares it in constant time.
67
+ * - Accepts when any `v1` matches the signature computed with any of
68
+ * `secret` — so a receiver mid-rotation can pass both of its secrets, and a
69
+ * delivery signed during the sender's overlap window verifies against
70
+ * either.
59
71
  */
60
72
  function verifyWebhook(secret, rawBody, header, toleranceSec, nowSec) {
61
73
  const parsed = parseHeader(header);
@@ -63,6 +75,17 @@ function verifyWebhook(secret, rawBody, header, toleranceSec, nowSec) {
63
75
  return false;
64
76
  if (Math.abs(nowSec - parsed.timestamp) > toleranceSec)
65
77
  return false;
66
- const expected = computeSignature(secret, rawBody, parsed.timestamp);
67
- return constantTimeEqualHex(expected, parsed.signature);
78
+ const secrets = typeof secret === 'string' ? [secret] : secret;
79
+ let matched = false;
80
+ // Every comparison runs, so the time taken does not say which one matched.
81
+ for (const candidate of secrets) {
82
+ if (candidate.length === 0)
83
+ continue;
84
+ const expected = computeSignature(candidate, rawBody, parsed.timestamp);
85
+ for (const signature of parsed.signatures) {
86
+ if (constantTimeEqualHex(expected, signature))
87
+ matched = true;
88
+ }
89
+ }
90
+ return matched;
68
91
  }
@@ -0,0 +1,50 @@
1
+ /** How a price's amount is computed from a quantity. */
2
+ export const PRICE_BILLING_SCHEMES = ['per_unit', 'tiered'];
3
+ /**
4
+ * How a tiered price reads its tiers.
5
+ *
6
+ * - `volume` — the WHOLE quantity is priced at the tier it falls into.
7
+ * - `graduated` — each unit is priced at the tier it falls into, so a
8
+ * quantity crossing two tiers pays each tier's rate for its
9
+ * share, plus the flat amount of every tier it reaches.
10
+ */
11
+ export const PRICE_TIERS_MODES = ['graduated', 'volume'];
12
+ /** `recurring` when the price has a recurring component, `one_time` otherwise. */
13
+ export const PRICE_TYPES = ['one_time', 'recurring'];
14
+ export const RECURRING_INTERVALS = ['day', 'week', 'month', 'year'];
15
+ /**
16
+ * `licensed` bills the quantity on the subscription. `metered` bills reported
17
+ * usage and needs a meter; meters are not part of Orbit yet, so the gateway
18
+ * refuses a metered price rather than store one nothing can bill.
19
+ */
20
+ export const RECURRING_USAGE_TYPES = ['licensed', 'metered'];
21
+ /**
22
+ * Whether a price includes tax (Peable Tax, ADR 0011). `unspecified` defers to
23
+ * the merchant's default tax behavior; once a price says `inclusive` or
24
+ * `exclusive` it can never change.
25
+ */
26
+ export const PRICE_TAX_BEHAVIORS = ['inclusive', 'exclusive', 'unspecified'];
27
+ export const TRANSFORM_QUANTITY_ROUNDS = ['up', 'down'];
28
+ /** How long a coupon's discount lasts once applied to a subscription. */
29
+ export const COUPON_DURATIONS = ['once', 'repeating', 'forever'];
30
+ /** The bounds the gateway enforces, published so a form can say them first. */
31
+ export const CATALOG_LIMITS = Object.freeze({
32
+ productNameMax: 250,
33
+ productDescriptionMax: 40000,
34
+ productImagesMax: 8,
35
+ urlMax: 2048,
36
+ unitLabelMax: 12,
37
+ statementDescriptorMax: 22,
38
+ marketingFeaturesMax: 15,
39
+ marketingFeatureNameMax: 80,
40
+ priceNicknameMax: 250,
41
+ lookupKeyMax: 200,
42
+ lookupKeysFilterMax: 10,
43
+ priceTiersMax: 250,
44
+ couponIdMax: 64,
45
+ couponNameMax: 40,
46
+ couponAppliesToProductsMax: 100,
47
+ couponDurationInMonthsMax: 1200,
48
+ promotionCodeMax: 64,
49
+ idsFilterMax: 100,
50
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};