@peable.to/sdk 0.2.6-oxy-one.0 → 0.2.7

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,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContributionsResource = void 0;
4
+ // Browser entry (`@peable.to/sdk/contributions`) — organization contribution
5
+ // management over a caller-supplied transport (the wallet's Oxy linked client).
6
+ // Deliberately separate from the server entry, which carries the merchant-authed
7
+ // service-token and billing-verification code (`node:crypto`) a browser or
8
+ // React Native bundle must never pull in.
9
+ var contributions_1 = require("./resources/contributions");
10
+ Object.defineProperty(exports, "ContributionsResource", { enumerable: true, get: function () { return contributions_1.ContributionsResource; } });
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DISPLAY_CURRENCIES = void 0;
4
+ exports.isDisplayCurrency = isDisplayCurrency;
5
+ exports.displayRatesAreFresh = displayRatesAreFresh;
6
+ exports.readDisplayRates = readDisplayRates;
7
+ exports.formatDisplayUnits = formatDisplayUnits;
8
+ exports.convertDisplayAmount = convertDisplayAmount;
9
+ /** Read-only display values. Never use these estimates to authorize a payment. */
10
+ const core_1 = require("@fairco.in/core");
11
+ exports.DISPLAY_CURRENCIES = ['USD', 'EUR', 'FAIR'];
12
+ function isDisplayCurrency(value) {
13
+ return exports.DISPLAY_CURRENCIES.some((currency) => currency === value);
14
+ }
15
+ function fraction(value) {
16
+ const match = /^(0|[1-9][0-9]*)(?:\.([0-9]+))?(?:e([+-]?[0-9]+))?$/i.exec(value);
17
+ if (!match || value.length > 80)
18
+ throw new Error('Invalid display rate');
19
+ const exponent = Number(match[3] ?? 0) - (match[2]?.length ?? 0);
20
+ if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 30)
21
+ throw new Error('Invalid display rate');
22
+ let numerator = BigInt(`${match[1]}${match[2] ?? ''}`);
23
+ let denominator = 1n;
24
+ if (exponent >= 0)
25
+ numerator *= 10n ** BigInt(exponent);
26
+ else
27
+ denominator = 10n ** BigInt(-exponent);
28
+ if (numerator <= 0n)
29
+ throw new Error('Invalid display rate');
30
+ return { numerator, denominator };
31
+ }
32
+ function displayRatesAreFresh(rates, now = Date.now()) {
33
+ const observed = Date.parse(rates.observedAt);
34
+ const expires = Date.parse(rates.expiresAt);
35
+ return (Number.isFinite(observed) &&
36
+ observed <= now + 1000 &&
37
+ observed >= now - 120000 &&
38
+ expires > now &&
39
+ expires <= observed + 120000);
40
+ }
41
+ /** Uses the owning FairCoin source; no cached fallback after expiry or failure. */
42
+ async function readDisplayRates(options = {}) {
43
+ const source = `${core_1.EXPLORER_BASE_URL}/api/price`;
44
+ const response = await (options.fetch ?? fetch)(source, {
45
+ signal: options.signal
46
+ ? AbortSignal.any([options.signal, AbortSignal.timeout(8000)])
47
+ : AbortSignal.timeout(8000),
48
+ redirect: 'error',
49
+ headers: { Accept: 'application/json' },
50
+ });
51
+ if (!response.ok)
52
+ throw new Error('Exchange rate unavailable');
53
+ const body = await response.json();
54
+ if (!body || typeof body !== 'object' || !('price' in body) || !('updatedAt' in body))
55
+ throw new Error('Exchange rate unavailable');
56
+ const price = body.price;
57
+ if ((typeof price !== 'number' && typeof price !== 'string') ||
58
+ !('source' in body) ||
59
+ body.source !== 'wfair-base' ||
60
+ typeof body.updatedAt !== 'string')
61
+ throw new Error('Exchange rate unavailable');
62
+ const usdPerFair = String(price);
63
+ fraction(usdPerFair);
64
+ const observed = Date.parse(body.updatedAt);
65
+ if (!Number.isFinite(observed))
66
+ throw new Error('Exchange rate unavailable');
67
+ const rates = {
68
+ usdPerFair,
69
+ // The public Explorer supplies USD only. Keep USD/FAIR usable without
70
+ // pretending the USD rate also applies to EUR or inventing a cross rate.
71
+ eurPerFair: null,
72
+ source,
73
+ observedAt: new Date(observed).toISOString(),
74
+ expiresAt: new Date(observed + 120000).toISOString(),
75
+ };
76
+ if (!displayRatesAreFresh(rates, options.now ?? Date.now()))
77
+ throw new Error('Exchange rate unavailable');
78
+ return rates;
79
+ }
80
+ /** Decimal string formatting preserves all eight FAIR decimals and large values. */
81
+ function formatDisplayUnits(units, decimals, currency) {
82
+ if (units < 0n || !Number.isInteger(decimals) || decimals < 0 || decimals > 8)
83
+ throw new Error('Invalid display amount');
84
+ const digits = units.toString().padStart(decimals + 1, '0');
85
+ return `${decimals ? `${digits.slice(0, -decimals)}.${digits.slice(-decimals)}` : digits} ${currency}`;
86
+ }
87
+ /** Round half up once, at the destination currency's precision. */
88
+ function convertDisplayAmount(input) {
89
+ if (!/^(0|[1-9][0-9]*)$/.test(input.units) || input.units.length > 30)
90
+ throw new Error('Invalid display amount');
91
+ const units = BigInt(input.units);
92
+ const sourceScale = input.currency === 'FAIR' ? 100000000n : 100n;
93
+ const targetDecimals = input.displayCurrency === 'FAIR' ? 8 : 2;
94
+ if (input.currency === input.displayCurrency || units === 0n)
95
+ return formatDisplayUnits(units === 0n ? 0n : units, targetDecimals, input.displayCurrency);
96
+ if (!input.rates || !displayRatesAreFresh(input.rates, input.now))
97
+ return null;
98
+ const destinationRate = input.displayCurrency === 'EUR' ? input.rates.eurPerFair : input.rates.usdPerFair;
99
+ if (destinationRate === null)
100
+ return null;
101
+ const usd = fraction(input.rates.usdPerFair);
102
+ const destination = input.displayCurrency === 'FAIR'
103
+ ? { numerator: 1n, denominator: 1n }
104
+ : fraction(destinationRate);
105
+ const numerator = units *
106
+ destination.numerator *
107
+ 10n ** BigInt(targetDecimals) *
108
+ (input.currency === 'USD' ? usd.denominator : 1n);
109
+ const denominator = sourceScale * destination.denominator * (input.currency === 'USD' ? usd.numerator : 1n);
110
+ return formatDisplayUnits((numerator * 2n + denominator) / (2n * denominator), targetDecimals, input.displayCurrency);
111
+ }
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WebhooksResource = exports.WEBHOOK_SIGNATURE_HEADER = exports.PeableSignatureVerificationError = exports.PeableApiError = exports.PeablePermissionError = exports.PeableInvalidRequestError = exports.PeableAuthenticationError = exports.PeableError = exports.DEFAULT_OXY_API_URL = exports.DEFAULT_GATEWAY_BASE_URL = exports.Peable = void 0;
3
+ exports.ContributionsResource = exports.WebhooksResource = exports.WEBHOOK_SIGNATURE_HEADER = exports.PeableSignatureVerificationError = exports.PeableApiError = exports.PeablePermissionError = exports.PeableInvalidRequestError = exports.PeableAuthenticationError = exports.PeableError = exports.DEFAULT_OXY_API_URL = exports.DEFAULT_GATEWAY_BASE_URL = exports.Peable = void 0;
4
4
  const billing_1 = require("./resources/billing");
5
5
  // Server entry (`@peable.to/sdk`) — the merchant-authed SDK. Configured with a
6
6
  // confidential `ApplicationCredential` (`{publicKey, secret}`), from which it
@@ -70,3 +70,5 @@ Object.defineProperty(exports, "PeableSignatureVerificationError", { enumerable:
70
70
  var webhooks_2 = require("./resources/webhooks");
71
71
  Object.defineProperty(exports, "WEBHOOK_SIGNATURE_HEADER", { enumerable: true, get: function () { return webhooks_2.WEBHOOK_SIGNATURE_HEADER; } });
72
72
  Object.defineProperty(exports, "WebhooksResource", { enumerable: true, get: function () { return webhooks_2.WebhooksResource; } });
73
+ var contributions_1 = require("./resources/contributions");
74
+ Object.defineProperty(exports, "ContributionsResource", { enumerable: true, get: function () { return contributions_1.ContributionsResource; } });
@@ -25,6 +25,13 @@ class BillingResource {
25
25
  idempotencyKey: options.idempotencyKey,
26
26
  });
27
27
  }
28
+ /** Opens a new expiring FAIR payment for one monthly period. Never auto-debits. */
29
+ renewSubscription(ref, params, options) {
30
+ return this.client.request('POST', `/v1/billing/subscriptions/${encodeURIComponent(ref)}/renew`, {
31
+ body: params,
32
+ idempotencyKey: options.idempotencyKey,
33
+ });
34
+ }
28
35
  createPortalSession(params, options) {
29
36
  return this.client.request('POST', '/v1/billing/portal_sessions', {
30
37
  body: params,
@@ -34,6 +41,12 @@ class BillingResource {
34
41
  retrieveCheckout(ref) {
35
42
  return this.client.request('GET', `/v1/billing/checkout_sessions/${encodeURIComponent(ref)}`);
36
43
  }
44
+ /** Reads existing operation state only, even after quote/session expiry.
45
+ * Missing/pending/indeterminate never authorizes another create or cancellation. */
46
+ retrieveCheckoutOperation(params) {
47
+ const { idempotencyKey, ...query } = params;
48
+ return this.client.request('GET', `/v1/billing/checkout_operations/${encodeURIComponent(idempotencyKey)}`, { query });
49
+ }
37
50
  async createTaxQuote(params) {
38
51
  if (!this.taxQuoteAuthorityKeys || Object.keys(this.taxQuoteAuthorityKeys).length === 0)
39
52
  throw new Error('Peable: tax quote authority verification is unconfigured');
@@ -90,7 +103,7 @@ class BillingResource {
90
103
  !Number.isFinite(Date.parse(quote.quotedAt)) ||
91
104
  Date.parse(quote.quotedAt) > Date.now() ||
92
105
  !Number.isFinite(Date.parse(quote.expiresAt)) ||
93
- Date.parse(quote.expiresAt) <= Date.now() ||
106
+ Date.parse(quote.expiresAt) <= Date.parse(quote.quotedAt) ||
94
107
  Date.parse(quote.expiresAt) > Date.parse(authority.invoice.expiresAt))
95
108
  throw new Error('Invalid quote');
96
109
  }
@@ -98,6 +111,25 @@ class BillingResource {
98
111
  if (key.asymmetricKeyType !== 'ed25519' ||
99
112
  !(0, node_crypto_1.verify)(null, Buffer.from((0, shared_types_1.canonicalBillingAuthority)(authority), 'utf8'), key, Buffer.from(authority.signature.value, 'base64url')))
100
113
  throw new Error('Invalid signature');
114
+ // The FX quote bounds the payment, not delivery of its observation. A
115
+ // delayed webhook must remain recoverable after the original quote expires.
116
+ if (authority.method === 'faircoin') {
117
+ const paid = await this.retrievePaidInvoice(subscriptionRef, invoiceRef);
118
+ // biome-ignore lint/style/noNonNullAssertion: FairCoin quote presence and bounds were verified above before signature verification.
119
+ const quote = authority.invoice.faircoinQuote;
120
+ const paidAt = Date.parse(paid.paidAt);
121
+ if (paid.invoiceId !== invoiceRef ||
122
+ paid.providerSubscriptionId !== subscriptionRef ||
123
+ paid.paymentIntentId !== authority.source.paymentIntentId ||
124
+ paid.providerCustomerId !== authority.source.customerId ||
125
+ paid.providerPriceId !== authority.source.priceId ||
126
+ paid.storeId !== authority.invoice.context.payerAccountId ||
127
+ !Number.isFinite(paidAt) ||
128
+ paidAt > Date.now() ||
129
+ paidAt < Date.parse(quote.quotedAt) ||
130
+ paidAt >= Date.parse(quote.expiresAt))
131
+ throw new Error('Payment outside signed quote');
132
+ }
101
133
  }
102
134
  catch {
103
135
  throw new Error('Peable: invoice authority verification failed');
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContributionsResource = void 0;
4
+ const contracts_1 = require("@peable.to/contracts");
5
+ /** Human management uses an Oxy linked-client transport; never a merchant service token. */
6
+ class ContributionsResource {
7
+ constructor(client) {
8
+ this.client = client;
9
+ }
10
+ path(scope) {
11
+ return `/v1/organizations/${encodeURIComponent(scope.organizationId)}/applications/${encodeURIComponent(scope.applicationId)}/contributions`;
12
+ }
13
+ async create(scope, body, idempotencyKey) {
14
+ return contracts_1.contributionLinkSchema.parse(await this.client.request('POST', this.path(scope), {
15
+ query: { environment: scope.environment },
16
+ body: contracts_1.createContributionSchema.parse(body),
17
+ idempotencyKey: contracts_1.contributionOperationKeySchema.parse(idempotencyKey),
18
+ }));
19
+ }
20
+ async list(scope, page = {}) {
21
+ return contracts_1.contributionListSchema.parse(await this.client.request('GET', this.path(scope), {
22
+ query: { environment: scope.environment, ...page },
23
+ }));
24
+ }
25
+ async *iterate(scope) {
26
+ let cursor;
27
+ const seen = new Set();
28
+ do {
29
+ const page = await this.list(scope, cursor ? { cursor } : {});
30
+ yield* page.items;
31
+ cursor = page.nextCursor ?? undefined;
32
+ if (cursor) {
33
+ if (seen.has(cursor))
34
+ throw new Error('Repeated contribution cursor');
35
+ seen.add(cursor);
36
+ }
37
+ } while (cursor);
38
+ }
39
+ async setActive(scope, id, active) {
40
+ return contracts_1.contributionLinkSchema.parse(await this.client.request('PATCH', `${this.path(scope)}/${encodeURIComponent(id)}`, {
41
+ query: { environment: scope.environment },
42
+ body: { active },
43
+ }));
44
+ }
45
+ async collections(scope, id, page = {}) {
46
+ return contracts_1.contributionCollectionListSchema.parse(await this.client.request('GET', `${this.path(scope)}/${encodeURIComponent(id)}/collections`, { query: { environment: scope.environment, ...page } }));
47
+ }
48
+ }
49
+ exports.ContributionsResource = ContributionsResource;
@@ -36,6 +36,7 @@ function isWebhookEventShape(value) {
36
36
  const valid = typeof candidate.id === 'string' &&
37
37
  candidate.object === 'event' &&
38
38
  typeof candidate.type === 'string' &&
39
+ // biome-ignore lint/suspicious/noPrototypeBuiltins: the published SDK targets ES2020, and Object.hasOwn is ES2022
39
40
  Object.prototype.hasOwnProperty.call(WEBHOOK_EVENT_TYPES, candidate.type) &&
40
41
  typeof candidate.created === 'string' &&
41
42
  typeof candidate.data === 'object' &&
@@ -0,0 +1,6 @@
1
+ // Browser entry (`@peable.to/sdk/contributions`) — organization contribution
2
+ // management over a caller-supplied transport (the wallet's Oxy linked client).
3
+ // Deliberately separate from the server entry, which carries the merchant-authed
4
+ // service-token and billing-verification code (`node:crypto`) a browser or
5
+ // React Native bundle must never pull in.
6
+ export { ContributionsResource } from './resources/contributions.js';
@@ -0,0 +1,103 @@
1
+ /** Read-only display values. Never use these estimates to authorize a payment. */
2
+ import { EXPLORER_BASE_URL } from '@fairco.in/core';
3
+ export const DISPLAY_CURRENCIES = ['USD', 'EUR', 'FAIR'];
4
+ export function isDisplayCurrency(value) {
5
+ return DISPLAY_CURRENCIES.some((currency) => currency === value);
6
+ }
7
+ function fraction(value) {
8
+ const match = /^(0|[1-9][0-9]*)(?:\.([0-9]+))?(?:e([+-]?[0-9]+))?$/i.exec(value);
9
+ if (!match || value.length > 80)
10
+ throw new Error('Invalid display rate');
11
+ const exponent = Number(match[3] ?? 0) - (match[2]?.length ?? 0);
12
+ if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 30)
13
+ throw new Error('Invalid display rate');
14
+ let numerator = BigInt(`${match[1]}${match[2] ?? ''}`);
15
+ let denominator = 1n;
16
+ if (exponent >= 0)
17
+ numerator *= 10n ** BigInt(exponent);
18
+ else
19
+ denominator = 10n ** BigInt(-exponent);
20
+ if (numerator <= 0n)
21
+ throw new Error('Invalid display rate');
22
+ return { numerator, denominator };
23
+ }
24
+ export function displayRatesAreFresh(rates, now = Date.now()) {
25
+ const observed = Date.parse(rates.observedAt);
26
+ const expires = Date.parse(rates.expiresAt);
27
+ return (Number.isFinite(observed) &&
28
+ observed <= now + 1000 &&
29
+ observed >= now - 120000 &&
30
+ expires > now &&
31
+ expires <= observed + 120000);
32
+ }
33
+ /** Uses the owning FairCoin source; no cached fallback after expiry or failure. */
34
+ export async function readDisplayRates(options = {}) {
35
+ const source = `${EXPLORER_BASE_URL}/api/price`;
36
+ const response = await (options.fetch ?? fetch)(source, {
37
+ signal: options.signal
38
+ ? AbortSignal.any([options.signal, AbortSignal.timeout(8000)])
39
+ : AbortSignal.timeout(8000),
40
+ redirect: 'error',
41
+ headers: { Accept: 'application/json' },
42
+ });
43
+ if (!response.ok)
44
+ throw new Error('Exchange rate unavailable');
45
+ const body = await response.json();
46
+ if (!body || typeof body !== 'object' || !('price' in body) || !('updatedAt' in body))
47
+ throw new Error('Exchange rate unavailable');
48
+ const price = body.price;
49
+ if ((typeof price !== 'number' && typeof price !== 'string') ||
50
+ !('source' in body) ||
51
+ body.source !== 'wfair-base' ||
52
+ typeof body.updatedAt !== 'string')
53
+ throw new Error('Exchange rate unavailable');
54
+ const usdPerFair = String(price);
55
+ fraction(usdPerFair);
56
+ const observed = Date.parse(body.updatedAt);
57
+ if (!Number.isFinite(observed))
58
+ throw new Error('Exchange rate unavailable');
59
+ const rates = {
60
+ usdPerFair,
61
+ // The public Explorer supplies USD only. Keep USD/FAIR usable without
62
+ // pretending the USD rate also applies to EUR or inventing a cross rate.
63
+ eurPerFair: null,
64
+ source,
65
+ observedAt: new Date(observed).toISOString(),
66
+ expiresAt: new Date(observed + 120000).toISOString(),
67
+ };
68
+ if (!displayRatesAreFresh(rates, options.now ?? Date.now()))
69
+ throw new Error('Exchange rate unavailable');
70
+ return rates;
71
+ }
72
+ /** Decimal string formatting preserves all eight FAIR decimals and large values. */
73
+ export function formatDisplayUnits(units, decimals, currency) {
74
+ if (units < 0n || !Number.isInteger(decimals) || decimals < 0 || decimals > 8)
75
+ throw new Error('Invalid display amount');
76
+ const digits = units.toString().padStart(decimals + 1, '0');
77
+ return `${decimals ? `${digits.slice(0, -decimals)}.${digits.slice(-decimals)}` : digits} ${currency}`;
78
+ }
79
+ /** Round half up once, at the destination currency's precision. */
80
+ export function convertDisplayAmount(input) {
81
+ if (!/^(0|[1-9][0-9]*)$/.test(input.units) || input.units.length > 30)
82
+ throw new Error('Invalid display amount');
83
+ const units = BigInt(input.units);
84
+ const sourceScale = input.currency === 'FAIR' ? 100000000n : 100n;
85
+ const targetDecimals = input.displayCurrency === 'FAIR' ? 8 : 2;
86
+ if (input.currency === input.displayCurrency || units === 0n)
87
+ return formatDisplayUnits(units === 0n ? 0n : units, targetDecimals, input.displayCurrency);
88
+ if (!input.rates || !displayRatesAreFresh(input.rates, input.now))
89
+ return null;
90
+ const destinationRate = input.displayCurrency === 'EUR' ? input.rates.eurPerFair : input.rates.usdPerFair;
91
+ if (destinationRate === null)
92
+ return null;
93
+ const usd = fraction(input.rates.usdPerFair);
94
+ const destination = input.displayCurrency === 'FAIR'
95
+ ? { numerator: 1n, denominator: 1n }
96
+ : fraction(destinationRate);
97
+ const numerator = units *
98
+ destination.numerator *
99
+ 10n ** BigInt(targetDecimals) *
100
+ (input.currency === 'USD' ? usd.denominator : 1n);
101
+ const denominator = sourceScale * destination.denominator * (input.currency === 'USD' ? usd.numerator : 1n);
102
+ return formatDisplayUnits((numerator * 2n + denominator) / (2n * denominator), targetDecimals, input.displayCurrency);
103
+ }
package/dist/esm/index.js CHANGED
@@ -56,3 +56,4 @@ export class Peable {
56
56
  export { DEFAULT_GATEWAY_BASE_URL, DEFAULT_OXY_API_URL } from './core/config.js';
57
57
  export { PeableError, PeableAuthenticationError, PeableInvalidRequestError, PeablePermissionError, PeableApiError, PeableSignatureVerificationError, } from './core/errors.js';
58
58
  export { WEBHOOK_SIGNATURE_HEADER, WebhooksResource } from './resources/webhooks.js';
59
+ export { ContributionsResource } from './resources/contributions.js';
@@ -22,6 +22,13 @@ export class BillingResource {
22
22
  idempotencyKey: options.idempotencyKey,
23
23
  });
24
24
  }
25
+ /** Opens a new expiring FAIR payment for one monthly period. Never auto-debits. */
26
+ renewSubscription(ref, params, options) {
27
+ return this.client.request('POST', `/v1/billing/subscriptions/${encodeURIComponent(ref)}/renew`, {
28
+ body: params,
29
+ idempotencyKey: options.idempotencyKey,
30
+ });
31
+ }
25
32
  createPortalSession(params, options) {
26
33
  return this.client.request('POST', '/v1/billing/portal_sessions', {
27
34
  body: params,
@@ -31,6 +38,12 @@ export class BillingResource {
31
38
  retrieveCheckout(ref) {
32
39
  return this.client.request('GET', `/v1/billing/checkout_sessions/${encodeURIComponent(ref)}`);
33
40
  }
41
+ /** Reads existing operation state only, even after quote/session expiry.
42
+ * Missing/pending/indeterminate never authorizes another create or cancellation. */
43
+ retrieveCheckoutOperation(params) {
44
+ const { idempotencyKey, ...query } = params;
45
+ return this.client.request('GET', `/v1/billing/checkout_operations/${encodeURIComponent(idempotencyKey)}`, { query });
46
+ }
34
47
  async createTaxQuote(params) {
35
48
  if (!this.taxQuoteAuthorityKeys || Object.keys(this.taxQuoteAuthorityKeys).length === 0)
36
49
  throw new Error('Peable: tax quote authority verification is unconfigured');
@@ -87,7 +100,7 @@ export class BillingResource {
87
100
  !Number.isFinite(Date.parse(quote.quotedAt)) ||
88
101
  Date.parse(quote.quotedAt) > Date.now() ||
89
102
  !Number.isFinite(Date.parse(quote.expiresAt)) ||
90
- Date.parse(quote.expiresAt) <= Date.now() ||
103
+ Date.parse(quote.expiresAt) <= Date.parse(quote.quotedAt) ||
91
104
  Date.parse(quote.expiresAt) > Date.parse(authority.invoice.expiresAt))
92
105
  throw new Error('Invalid quote');
93
106
  }
@@ -95,6 +108,25 @@ export class BillingResource {
95
108
  if (key.asymmetricKeyType !== 'ed25519' ||
96
109
  !verify(null, Buffer.from(canonicalBillingAuthority(authority), 'utf8'), key, Buffer.from(authority.signature.value, 'base64url')))
97
110
  throw new Error('Invalid signature');
111
+ // The FX quote bounds the payment, not delivery of its observation. A
112
+ // delayed webhook must remain recoverable after the original quote expires.
113
+ if (authority.method === 'faircoin') {
114
+ const paid = await this.retrievePaidInvoice(subscriptionRef, invoiceRef);
115
+ // biome-ignore lint/style/noNonNullAssertion: FairCoin quote presence and bounds were verified above before signature verification.
116
+ const quote = authority.invoice.faircoinQuote;
117
+ const paidAt = Date.parse(paid.paidAt);
118
+ if (paid.invoiceId !== invoiceRef ||
119
+ paid.providerSubscriptionId !== subscriptionRef ||
120
+ paid.paymentIntentId !== authority.source.paymentIntentId ||
121
+ paid.providerCustomerId !== authority.source.customerId ||
122
+ paid.providerPriceId !== authority.source.priceId ||
123
+ paid.storeId !== authority.invoice.context.payerAccountId ||
124
+ !Number.isFinite(paidAt) ||
125
+ paidAt > Date.now() ||
126
+ paidAt < Date.parse(quote.quotedAt) ||
127
+ paidAt >= Date.parse(quote.expiresAt))
128
+ throw new Error('Payment outside signed quote');
129
+ }
98
130
  }
99
131
  catch {
100
132
  throw new Error('Peable: invoice authority verification failed');
@@ -0,0 +1,45 @@
1
+ import { contributionLinkSchema, contributionListSchema, contributionCollectionListSchema, createContributionSchema, contributionOperationKeySchema, } from '@peable.to/contracts';
2
+ /** Human management uses an Oxy linked-client transport; never a merchant service token. */
3
+ export class ContributionsResource {
4
+ constructor(client) {
5
+ this.client = client;
6
+ }
7
+ path(scope) {
8
+ return `/v1/organizations/${encodeURIComponent(scope.organizationId)}/applications/${encodeURIComponent(scope.applicationId)}/contributions`;
9
+ }
10
+ async create(scope, body, idempotencyKey) {
11
+ return contributionLinkSchema.parse(await this.client.request('POST', this.path(scope), {
12
+ query: { environment: scope.environment },
13
+ body: createContributionSchema.parse(body),
14
+ idempotencyKey: contributionOperationKeySchema.parse(idempotencyKey),
15
+ }));
16
+ }
17
+ async list(scope, page = {}) {
18
+ return contributionListSchema.parse(await this.client.request('GET', this.path(scope), {
19
+ query: { environment: scope.environment, ...page },
20
+ }));
21
+ }
22
+ async *iterate(scope) {
23
+ let cursor;
24
+ const seen = new Set();
25
+ do {
26
+ const page = await this.list(scope, cursor ? { cursor } : {});
27
+ yield* page.items;
28
+ cursor = page.nextCursor ?? undefined;
29
+ if (cursor) {
30
+ if (seen.has(cursor))
31
+ throw new Error('Repeated contribution cursor');
32
+ seen.add(cursor);
33
+ }
34
+ } while (cursor);
35
+ }
36
+ async setActive(scope, id, active) {
37
+ return contributionLinkSchema.parse(await this.client.request('PATCH', `${this.path(scope)}/${encodeURIComponent(id)}`, {
38
+ query: { environment: scope.environment },
39
+ body: { active },
40
+ }));
41
+ }
42
+ async collections(scope, id, page = {}) {
43
+ return contributionCollectionListSchema.parse(await this.client.request('GET', `${this.path(scope)}/${encodeURIComponent(id)}/collections`, { query: { environment: scope.environment, ...page } }));
44
+ }
45
+ }
@@ -33,6 +33,7 @@ function isWebhookEventShape(value) {
33
33
  const valid = typeof candidate.id === 'string' &&
34
34
  candidate.object === 'event' &&
35
35
  typeof candidate.type === 'string' &&
36
+ // biome-ignore lint/suspicious/noPrototypeBuiltins: the published SDK targets ES2020, and Object.hasOwn is ES2022
36
37
  Object.prototype.hasOwnProperty.call(WEBHOOK_EVENT_TYPES, candidate.type) &&
37
38
  typeof candidate.created === 'string' &&
38
39
  typeof candidate.data === 'object' &&
@@ -0,0 +1,2 @@
1
+ export { ContributionsResource, type ContributionScope } from './resources/contributions';
2
+ export type { RestClient, RestClientRequestOptions } from './core/client';
@@ -0,0 +1,29 @@
1
+ export declare const DISPLAY_CURRENCIES: readonly ["USD", "EUR", "FAIR"];
2
+ export type DisplayCurrency = (typeof DISPLAY_CURRENCIES)[number];
3
+ export interface DisplayRates {
4
+ /** Currency units per FAIR, as exact decimal strings. */
5
+ usdPerFair: string;
6
+ /** Null when the owning source does not supply a EUR quote. */
7
+ eurPerFair: string | null;
8
+ source: string;
9
+ observedAt: string;
10
+ expiresAt: string;
11
+ }
12
+ export declare function isDisplayCurrency(value: unknown): value is DisplayCurrency;
13
+ export declare function displayRatesAreFresh(rates: DisplayRates, now?: number): boolean;
14
+ /** Uses the owning FairCoin source; no cached fallback after expiry or failure. */
15
+ export declare function readDisplayRates(options?: {
16
+ fetch?: typeof fetch;
17
+ signal?: AbortSignal;
18
+ now?: number;
19
+ }): Promise<DisplayRates>;
20
+ /** Decimal string formatting preserves all eight FAIR decimals and large values. */
21
+ export declare function formatDisplayUnits(units: bigint, decimals: number, currency: string): string;
22
+ /** Round half up once, at the destination currency's precision. */
23
+ export declare function convertDisplayAmount(input: {
24
+ units: string;
25
+ currency: 'USD' | 'FAIR';
26
+ displayCurrency: DisplayCurrency;
27
+ rates?: DisplayRates | null;
28
+ now?: number;
29
+ }): string | null;
@@ -1,6 +1,6 @@
1
1
  import { BillingResource } from './resources/billing';
2
2
  export type { BillingRequestOptions } from './resources/billing';
3
- export type { CreateBillingTaxQuoteParams, BillingTaxQuote, BillingInvoiceAuthoritySource, BillingFinalInvoice, BillingFinalInvoiceAuthority, BillingFaircoinRenewalChoice, BillingFaircoinRenewalConsent, BillingFaircoinRenewalRevocation, BillingObservation, BillingCustomer, BillingHostedSession, BillingCheckoutSession, BillingCheckoutObservation, BillingPaidInvoice, BillingInvoiceState, BillingSubscription, CreateBillingCheckoutParams, CreateBillingPortalParams, EnsureBillingCustomerParams, } from '@peable.to/shared-types';
3
+ export type { CreateBillingTaxQuoteParams, BillingTaxQuote, BillingInvoiceAuthoritySource, BillingFinalInvoice, BillingFinalInvoiceAuthority, BillingFaircoinRenewalChoice, BillingFaircoinRenewalConsent, BillingFaircoinRenewalRevocation, BillingObservation, BillingCustomer, BillingHostedSession, BillingCheckoutSession, BillingCheckoutObservation, RetrieveBillingCheckoutOperationParams, BillingCheckoutOperationSource, BillingCheckoutOperationRecovery, BillingPaidInvoice, BillingInvoiceState, BillingSubscription, CreateBillingCheckoutParams, CreateBillingPortalParams, EnsureBillingCustomerParams, } from '@peable.to/shared-types';
4
4
  import { type PeableConfig } from './core/config';
5
5
  import { PaymentIntentsResource } from './resources/paymentIntents';
6
6
  import { PaymentLinksResource } from './resources/paymentLinks';
@@ -59,3 +59,6 @@ export type { CreateTransferParams, ReverseTransferParams, TransferList, } from
59
59
  export type { PaymentLinkListParams, PaymentLinkList, UpdatePaymentLinkParams, } from './resources/paymentLinks';
60
60
  export { WEBHOOK_SIGNATURE_HEADER, WebhooksResource } from './resources/webhooks';
61
61
  export type { ConstructEventOptions } from './resources/webhooks';
62
+ export { ContributionsResource } from './resources/contributions';
63
+ export type { ContributionScope, CreateContribution, ContributionLink, ContributionDetails, } from './resources/contributions';
64
+ export type { BillingFaircoinFxQuote } from '@peable.to/shared-types';
@@ -1,4 +1,4 @@
1
- import type { BillingTaxQuote, CreateBillingTaxQuoteParams, BillingFinalInvoiceAuthority, BillingCustomer, BillingHostedSession, BillingCheckoutSession, BillingCheckoutObservation, BillingPaidInvoice, BillingInvoiceState, BillingSubscription, CreateBillingCheckoutParams, CreateBillingPortalParams, EnsureBillingCustomerParams } from '@peable.to/shared-types';
1
+ import type { BillingTaxQuote, RetrieveBillingCheckoutOperationParams, BillingCheckoutOperationRecovery, CreateBillingTaxQuoteParams, BillingFinalInvoiceAuthority, BillingCustomer, BillingHostedSession, BillingCheckoutSession, BillingCheckoutObservation, BillingPaidInvoice, BillingInvoiceState, BillingSubscription, CreateBillingCheckoutParams, CreateBillingPortalParams, EnsureBillingCustomerParams } from '@peable.to/shared-types';
2
2
  import type { RestClient } from '../core/client';
3
3
  export interface BillingRequestOptions {
4
4
  idempotencyKey: string;
@@ -14,8 +14,13 @@ export declare class BillingResource {
14
14
  * its pinned authority and retains immutable evidence before creating a session.
15
15
  * Existing callers without a quote remain on their explicitly enabled legacy path. */
16
16
  createCheckoutSession(params: CreateBillingCheckoutParams, options: BillingRequestOptions): Promise<BillingCheckoutSession>;
17
+ /** Opens a new expiring FAIR payment for one monthly period. Never auto-debits. */
18
+ renewSubscription(ref: string, params: CreateBillingCheckoutParams, options: BillingRequestOptions): Promise<BillingCheckoutSession>;
17
19
  createPortalSession(params: CreateBillingPortalParams, options: BillingRequestOptions): Promise<BillingHostedSession>;
18
20
  retrieveCheckout(ref: string): Promise<BillingCheckoutObservation>;
21
+ /** Reads existing operation state only, even after quote/session expiry.
22
+ * Missing/pending/indeterminate never authorizes another create or cancellation. */
23
+ retrieveCheckoutOperation(params: RetrieveBillingCheckoutOperationParams): Promise<BillingCheckoutOperationRecovery>;
19
24
  createTaxQuote(params: CreateBillingTaxQuoteParams): Promise<BillingTaxQuote>;
20
25
  retrieveFinalInvoiceAuthority(subscriptionRef: string, invoiceRef: string): Promise<BillingFinalInvoiceAuthority>;
21
26
  retrievePaidInvoice(subscriptionRef: string, invoiceRef: string): Promise<BillingPaidInvoice>;
@@ -0,0 +1,100 @@
1
+ import { type CreateContribution } from '@peable.to/contracts';
2
+ import type { RestClient } from '../core/client';
3
+ export interface ContributionScope {
4
+ organizationId: string;
5
+ applicationId: string;
6
+ environment: 'development' | 'staging' | 'production';
7
+ }
8
+ /** Human management uses an Oxy linked-client transport; never a merchant service token. */
9
+ export declare class ContributionsResource {
10
+ private readonly client;
11
+ constructor(client: RestClient);
12
+ private path;
13
+ create(scope: ContributionScope, body: CreateContribution, idempotencyKey: string): Promise<{
14
+ id: string;
15
+ amount: string;
16
+ currency: "FAIR";
17
+ network: "mainnet" | "testnet";
18
+ active: boolean;
19
+ url: string;
20
+ contribution: {
21
+ organizationId: string;
22
+ purpose: string;
23
+ amountMode: "fixed" | "free";
24
+ expiresAt: string | null;
25
+ };
26
+ createdAt: string;
27
+ }>;
28
+ list(scope: ContributionScope, page?: {
29
+ cursor?: string;
30
+ limit?: number;
31
+ }): Promise<{
32
+ items: {
33
+ id: string;
34
+ amount: string;
35
+ currency: "FAIR";
36
+ network: "mainnet" | "testnet";
37
+ active: boolean;
38
+ url: string;
39
+ contribution: {
40
+ organizationId: string;
41
+ purpose: string;
42
+ amountMode: "fixed" | "free";
43
+ expiresAt: string | null;
44
+ };
45
+ createdAt: string;
46
+ }[];
47
+ nextCursor: string | null;
48
+ }>;
49
+ iterate(scope: ContributionScope): AsyncGenerator<{
50
+ id: string;
51
+ amount: string;
52
+ currency: "FAIR";
53
+ network: "mainnet" | "testnet";
54
+ active: boolean;
55
+ url: string;
56
+ contribution: {
57
+ organizationId: string;
58
+ purpose: string;
59
+ amountMode: "fixed" | "free";
60
+ expiresAt: string | null;
61
+ };
62
+ createdAt: string;
63
+ }, void, unknown>;
64
+ setActive(scope: ContributionScope, id: string, active: boolean): Promise<{
65
+ id: string;
66
+ amount: string;
67
+ currency: "FAIR";
68
+ network: "mainnet" | "testnet";
69
+ active: boolean;
70
+ url: string;
71
+ contribution: {
72
+ organizationId: string;
73
+ purpose: string;
74
+ amountMode: "fixed" | "free";
75
+ expiresAt: string | null;
76
+ };
77
+ createdAt: string;
78
+ }>;
79
+ collections(scope: ContributionScope, id: string, page?: {
80
+ cursor?: string;
81
+ limit?: number;
82
+ }): Promise<{
83
+ items: {
84
+ id: string;
85
+ amount: string;
86
+ currency: "FAIR";
87
+ status: string;
88
+ createdAt: string;
89
+ refundCapability: "unsupported_non_custodial";
90
+ refunds: {
91
+ id: string;
92
+ amount: string;
93
+ status: string;
94
+ createdAt: string;
95
+ }[];
96
+ }[];
97
+ nextCursor: string | null;
98
+ }>;
99
+ }
100
+ export type { CreateContribution, ContributionLink, ContributionDetails, } from '@peable.to/contracts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peable.to/sdk",
3
- "version": "0.2.6-oxy-one.0",
3
+ "version": "0.2.7",
4
4
  "description": "Peable SDK — a Stripe-ergonomics TypeScript client over the Peable Gateway REST + realtime contract. Server entry (`@peable.to/sdk`) mints Oxy service tokens from an ApplicationCredential and exposes paymentIntents/paymentLinks/checkout.sessions/webhooks; browser entry (`@peable.to/sdk/checkout`) is the anonymous payer-side client core.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -9,8 +9,7 @@
9
9
  "sideEffects": false,
10
10
  "publishConfig": {
11
11
  "access": "public",
12
- "registry": "https://registry.npmjs.org/",
13
- "tag": "oxy-one"
12
+ "registry": "https://registry.npmjs.org/"
14
13
  },
15
14
  "exports": {
16
15
  ".": {
@@ -37,7 +36,26 @@
37
36
  },
38
37
  "default": "./dist/esm/checkout.js"
39
38
  },
40
- "./package.json": "./package.json"
39
+ "./package.json": "./package.json",
40
+ "./display": {
41
+ "react-native": "./dist/esm/display.js",
42
+ "import": {
43
+ "types": "./dist/types/display.d.ts",
44
+ "default": "./dist/esm/display.js"
45
+ }
46
+ },
47
+ "./contributions": {
48
+ "react-native": "./dist/esm/contributions.js",
49
+ "import": {
50
+ "types": "./dist/types/contributions.d.ts",
51
+ "default": "./dist/esm/contributions.js"
52
+ },
53
+ "require": {
54
+ "types": "./dist/types/contributions.d.ts",
55
+ "default": "./dist/cjs/contributions.js"
56
+ },
57
+ "default": "./dist/esm/contributions.js"
58
+ }
41
59
  },
42
60
  "files": [
43
61
  "dist"
@@ -72,9 +90,11 @@
72
90
  "prepublishOnly": "bun run clean && bun run build"
73
91
  },
74
92
  "dependencies": {
75
- "@peable.to/shared-types": "0.3.3-oxy-one.0",
93
+ "@peable.to/shared-types": "0.3.4",
76
94
  "qrcode-generator": "^2.0.4",
77
- "socket.io-client": "^4.8.3"
95
+ "socket.io-client": "^4.8.3",
96
+ "@peable.to/contracts": "0.1.0",
97
+ "@fairco.in/core": "0.5.0"
78
98
  },
79
99
  "devDependencies": {
80
100
  "typescript": "~5.9.2",