@mystars-tg/faas-sdk 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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/index.cjs +1516 -0
- package/dist/index.d.cts +1038 -0
- package/dist/index.d.ts +1038 -0
- package/dist/index.js +1460 -0
- package/package.json +55 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the MyStars FaaS `/v1` API.
|
|
3
|
+
*
|
|
4
|
+
* These mirror the JSON the server actually returns, field-for-field, in
|
|
5
|
+
* `snake_case` — so they read identically to the REST docs. Money is always a
|
|
6
|
+
* decimal string (never a `number`); convert to smallest units only at the
|
|
7
|
+
* on-chain boundary.
|
|
8
|
+
*/
|
|
9
|
+
/** On-chain payment currency. Both settle on the TON chain. */
|
|
10
|
+
type Currency = "ton" | "usdt_ton";
|
|
11
|
+
/** Orderable product type. */
|
|
12
|
+
type OrderType = "stars" | "premium";
|
|
13
|
+
/** Which numeric parameter a product is sized by. */
|
|
14
|
+
type Parameter = "quantity" | "months";
|
|
15
|
+
/** The full FaaS order status domain (15 values). */
|
|
16
|
+
type OrderStatus = "received" | "awaiting_payment" | "paid" | "reserved" | "swapping" | "funding" | "purchasing" | "fulfilling" | "completed" | "delivered" | "failed" | "reversed" | "expired" | "held" | "cancelled";
|
|
17
|
+
/** Statuses an order can never leave. */
|
|
18
|
+
type TerminalStatus = "delivered" | "failed" | "reversed" | "expired" | "cancelled";
|
|
19
|
+
/** The terminal status set — an order in one of these will not change again. */
|
|
20
|
+
declare const TERMINAL_STATUSES: ReadonlySet<OrderStatus>;
|
|
21
|
+
/** True when an order has reached a final state. */
|
|
22
|
+
declare function isTerminal(status: OrderStatus): status is TerminalStatus;
|
|
23
|
+
/** A reason a recipient cannot receive the item. */
|
|
24
|
+
type RecipientCheckReason = "already_subscribed" | "not_found" | "ineligible";
|
|
25
|
+
/** A reason an order failed. */
|
|
26
|
+
type FailureReason = "underpaid" | "overpaid" | "no_memo" | "wrong_memo" | "undeliverable" | "expired" | (string & {});
|
|
27
|
+
/** One supported payment currency, from `GET /v1/currencies`. */
|
|
28
|
+
interface CurrencyInfo {
|
|
29
|
+
code: Currency;
|
|
30
|
+
chain: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
/** One product from the catalog, from `GET /v1/products`. */
|
|
34
|
+
interface Product {
|
|
35
|
+
type: OrderType;
|
|
36
|
+
name: string;
|
|
37
|
+
parameter: Parameter;
|
|
38
|
+
min: number;
|
|
39
|
+
max: number;
|
|
40
|
+
/** A fixed allowed set (e.g. premium months `[3,6,12]`), or `null` for a continuous range. */
|
|
41
|
+
values: number[] | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The USDT processing-fee itemization (present only for `usdt_ton`).
|
|
45
|
+
*
|
|
46
|
+
* Invariant: `subtotal + processing_fee === total === amount`. This is an
|
|
47
|
+
* itemization of the fee already inside `amount` — never an additional charge.
|
|
48
|
+
* All fields are decimal USDT strings.
|
|
49
|
+
*/
|
|
50
|
+
interface FeeBreakdown {
|
|
51
|
+
subtotal: string;
|
|
52
|
+
processing_fee: string;
|
|
53
|
+
total: string;
|
|
54
|
+
description: string;
|
|
55
|
+
currency: "usdt";
|
|
56
|
+
}
|
|
57
|
+
/** A price quote from `GET /v1/pricing`. */
|
|
58
|
+
interface PricingQuote {
|
|
59
|
+
type: OrderType;
|
|
60
|
+
quantity: number | null;
|
|
61
|
+
months: number | null;
|
|
62
|
+
/** All-in total to pay, in `currency`, as a decimal string. */
|
|
63
|
+
amount: string;
|
|
64
|
+
currency: Currency;
|
|
65
|
+
/** Itemized fee for `usdt_ton`; `null` for `ton`. */
|
|
66
|
+
fee: FeeBreakdown | null;
|
|
67
|
+
/** Informational market rate; `null` if the cache is cold. */
|
|
68
|
+
usdt_per_ton: string | null;
|
|
69
|
+
quoted_at: string;
|
|
70
|
+
/** A re-quote hint (`quoted_at + ttl`) — NOT a price lock. Price is fixed at order creation. */
|
|
71
|
+
valid_until: string;
|
|
72
|
+
}
|
|
73
|
+
/** The result of `POST /v1/recipients/check`. */
|
|
74
|
+
interface RecipientCheck {
|
|
75
|
+
resolved: boolean;
|
|
76
|
+
eligible: boolean;
|
|
77
|
+
recipient_name: string | null;
|
|
78
|
+
reason: RecipientCheckReason | null;
|
|
79
|
+
/** Verbatim Telegram/Fragment rejection text; `null` when eligible. */
|
|
80
|
+
telegram_message: string | null;
|
|
81
|
+
}
|
|
82
|
+
/** The on-chain payment instruction attached to a created order. */
|
|
83
|
+
interface PaymentInstruction {
|
|
84
|
+
currency: Currency;
|
|
85
|
+
chain: "ton";
|
|
86
|
+
/** The treasury OWNER address — the same for both `ton` and `usdt_ton`. */
|
|
87
|
+
pay_to_address: string | null;
|
|
88
|
+
/** The bare order UUID, attached as the on-chain text comment (no `STARS:` prefix). */
|
|
89
|
+
memo: string | null;
|
|
90
|
+
/** The exact amount to send, as a decimal string. */
|
|
91
|
+
amount: string;
|
|
92
|
+
amount_units: "ton" | "usdt";
|
|
93
|
+
/** Itemized fee for `usdt_ton`; `null` for `ton`. */
|
|
94
|
+
fee: FeeBreakdown | null;
|
|
95
|
+
}
|
|
96
|
+
/** The full order resource, from `GET /v1/orders` and `GET /v1/orders/:id`. */
|
|
97
|
+
interface Order {
|
|
98
|
+
order_id: string;
|
|
99
|
+
status: OrderStatus;
|
|
100
|
+
type: OrderType;
|
|
101
|
+
recipient_username: string | null;
|
|
102
|
+
quantity: number | null;
|
|
103
|
+
months: number | null;
|
|
104
|
+
amount_ton: string | null;
|
|
105
|
+
payment_tx: string | null;
|
|
106
|
+
purchase_tx: string | null;
|
|
107
|
+
failure_reason: FailureReason | null;
|
|
108
|
+
reversal_tx: string | null;
|
|
109
|
+
telegram_message: string | null;
|
|
110
|
+
created_at: string;
|
|
111
|
+
updated_at: string;
|
|
112
|
+
/** Non-null only while `status === "awaiting_payment"`. */
|
|
113
|
+
expires_at: string | null;
|
|
114
|
+
}
|
|
115
|
+
/** The result of `POST /v1/orders` (201 new, or 200 idempotent replay). */
|
|
116
|
+
interface CreateOrderResult {
|
|
117
|
+
order_id: string;
|
|
118
|
+
/**
|
|
119
|
+
* A fresh 201 create is always `awaiting_payment`. A 200 idempotent replay
|
|
120
|
+
* (same `Idempotency-Key`, same body) echoes the order's CURRENT status, which
|
|
121
|
+
* may already have advanced (e.g. `paid`, `delivered`) — so narrow against the
|
|
122
|
+
* full status domain, not the literal.
|
|
123
|
+
*/
|
|
124
|
+
status: OrderStatus;
|
|
125
|
+
type: OrderType;
|
|
126
|
+
quantity: number | null;
|
|
127
|
+
months: number | null;
|
|
128
|
+
payment: PaymentInstruction;
|
|
129
|
+
expires_at: string;
|
|
130
|
+
/** `true` when the server returned 200 (idempotent replay) rather than 201 (fresh create). */
|
|
131
|
+
replayed: boolean;
|
|
132
|
+
}
|
|
133
|
+
/** One page of orders from `GET /v1/orders`. */
|
|
134
|
+
interface OrdersPage {
|
|
135
|
+
orders: Order[];
|
|
136
|
+
/** Opaque keyset cursor for the next page, or `null` on the final page. */
|
|
137
|
+
next_cursor: string | null;
|
|
138
|
+
}
|
|
139
|
+
/** The terminal statuses that fire a webhook (NOT `cancelled`, which is a manual API action). */
|
|
140
|
+
type WebhookStatus = "delivered" | "failed" | "reversed" | "expired";
|
|
141
|
+
/**
|
|
142
|
+
* The terminal statuses that fire an order webhook — `delivered`/`failed`/
|
|
143
|
+
* `reversed`/`expired` (NOT `cancelled`, which is a manual API action and never
|
|
144
|
+
* webhooked). Mirrors `webhook_terminal` in the contract status-machine fixture.
|
|
145
|
+
*/
|
|
146
|
+
declare const WEBHOOK_TERMINAL: ReadonlySet<WebhookStatus>;
|
|
147
|
+
/** The status an order is in immediately after a fresh `POST /v1/orders` (201). */
|
|
148
|
+
declare const INITIAL_ORDER_STATUS: OrderStatus;
|
|
149
|
+
/** Statuses an order can be cancelled from via `POST /v1/orders/:id/cancel`. */
|
|
150
|
+
declare const CANCELLABLE_STATUSES: ReadonlySet<OrderStatus>;
|
|
151
|
+
/**
|
|
152
|
+
* The body of an order webhook (POSTed to your `callback_url`). Deliberately
|
|
153
|
+
* minimal — fetch `GET /v1/orders/:id` for full detail. Treat every field beyond
|
|
154
|
+
* `order_id`/`status` as optional.
|
|
155
|
+
*/
|
|
156
|
+
interface WebhookEvent {
|
|
157
|
+
order_id: string;
|
|
158
|
+
status: WebhookStatus;
|
|
159
|
+
failure_reason?: FailureReason;
|
|
160
|
+
purchase_tx?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Typed error taxonomy for the MyStars FaaS SDK.
|
|
165
|
+
*
|
|
166
|
+
* The server returns an envelope `{ error: { code, message, telegram_message? } }`
|
|
167
|
+
* for handled errors, and a bare `{ "error": "not_found" }` string for unmatched
|
|
168
|
+
* routes. `errorFromResponse` maps both forms — plus network/timeout failures —
|
|
169
|
+
* to the right class, keyed on the envelope `code` (the code, not the HTTP
|
|
170
|
+
* status, is authoritative; an unknown future code falls back to the base class
|
|
171
|
+
* so the SDK never crashes on a new code).
|
|
172
|
+
*/
|
|
173
|
+
|
|
174
|
+
/** Base class for every error this SDK throws. */
|
|
175
|
+
declare abstract class MyStarsError extends Error {
|
|
176
|
+
constructor(message: string);
|
|
177
|
+
}
|
|
178
|
+
interface ApiErrorInit {
|
|
179
|
+
code: string;
|
|
180
|
+
status: number;
|
|
181
|
+
message: string;
|
|
182
|
+
telegramMessage?: string | undefined;
|
|
183
|
+
requestId?: string | undefined;
|
|
184
|
+
retryable?: boolean;
|
|
185
|
+
raw?: unknown;
|
|
186
|
+
/** The `Idempotency-Key` that was sent with the failed request (set by `createOrder`). */
|
|
187
|
+
idempotencyKey?: string | undefined;
|
|
188
|
+
}
|
|
189
|
+
/** Any error originating from an HTTP response (or a failed attempt to make one). */
|
|
190
|
+
declare class MyStarsApiError extends MyStarsError {
|
|
191
|
+
/** The envelope `error.code`, or `"unknown"` / `"network"`. */
|
|
192
|
+
readonly code: string;
|
|
193
|
+
/** HTTP status code (`0` for a network/timeout failure). */
|
|
194
|
+
readonly status: number;
|
|
195
|
+
/** Buyer-facing Telegram/Fragment message, when the server supplied one. */
|
|
196
|
+
readonly telegramMessage?: string;
|
|
197
|
+
/** The `x-request-id` response header, when present. */
|
|
198
|
+
readonly requestId?: string;
|
|
199
|
+
/** Coarse hint that the failure is potentially transient. The retry policy decides for real. */
|
|
200
|
+
readonly retryable: boolean;
|
|
201
|
+
/** The parsed response body (or the thrown error), for debugging. */
|
|
202
|
+
readonly raw?: unknown;
|
|
203
|
+
/**
|
|
204
|
+
* The `Idempotency-Key` that was sent with the request that failed, when known.
|
|
205
|
+
*
|
|
206
|
+
* `createOrder` stamps this on a thrown error so you can SAFELY retry the
|
|
207
|
+
* create with the SAME key (`{ idempotencyKey: err.idempotencyKey }`) instead
|
|
208
|
+
* of minting a duplicate deliverable when you can't tell whether the order was
|
|
209
|
+
* created server-side. `undefined` for errors not raised by a keyed request.
|
|
210
|
+
*/
|
|
211
|
+
readonly idempotencyKey?: string;
|
|
212
|
+
constructor(init: ApiErrorInit);
|
|
213
|
+
}
|
|
214
|
+
/** 400 — malformed request, validation failure, or missing `Idempotency-Key`. */
|
|
215
|
+
declare class BadRequestError extends MyStarsApiError {
|
|
216
|
+
}
|
|
217
|
+
/** 401 — missing or invalid `X-Api-Key`. */
|
|
218
|
+
declare class UnauthorizedError extends MyStarsApiError {
|
|
219
|
+
}
|
|
220
|
+
/** 403 — the API key is valid but the tenant is suspended or banned. */
|
|
221
|
+
declare class ForbiddenError extends MyStarsApiError {
|
|
222
|
+
}
|
|
223
|
+
/** 404 — order not found (or an unmatched route). */
|
|
224
|
+
declare class NotFoundError extends MyStarsApiError {
|
|
225
|
+
}
|
|
226
|
+
/** 409 — a generic conflict. */
|
|
227
|
+
declare class ConflictError extends MyStarsApiError {
|
|
228
|
+
}
|
|
229
|
+
/** 409 — the same `Idempotency-Key` was reused with a different request body. */
|
|
230
|
+
declare class IdempotencyConflictError extends ConflictError {
|
|
231
|
+
}
|
|
232
|
+
/** 409 — the order is not in `awaiting_payment` and cannot be cancelled. */
|
|
233
|
+
declare class OrderNotCancellableError extends ConflictError {
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 422 — the recipient cannot receive the item; no order was created.
|
|
237
|
+
*
|
|
238
|
+
* The server's 422 body carries only `telegram_message` (the buyer-facing reason
|
|
239
|
+
* to show your user) — NOT a structured `reason` code. For the structured
|
|
240
|
+
* `reason` (`already_subscribed` | `not_found` | `ineligible`), call
|
|
241
|
+
* `client.checkRecipient(...)` first and read its `reason` field.
|
|
242
|
+
*/
|
|
243
|
+
declare class RecipientIneligibleError extends MyStarsApiError {
|
|
244
|
+
readonly telegramMessage: string;
|
|
245
|
+
constructor(init: ApiErrorInit);
|
|
246
|
+
}
|
|
247
|
+
/** Which limiter a {@link RateLimitError} came from: the per-minute limiter or the daily order-cap/flood guard. */
|
|
248
|
+
type RateLimitKind = "general" | "order_cap";
|
|
249
|
+
/** 429 — rate limited. `kind` distinguishes the per-minute limiter from the daily order cap / flood guard. */
|
|
250
|
+
declare class RateLimitError extends MyStarsApiError {
|
|
251
|
+
/** Milliseconds to wait before retrying, from `Retry-After`; `null` for the order-cap/flood limiter. */
|
|
252
|
+
readonly retryAfterMs: number | null;
|
|
253
|
+
readonly limit: number | null;
|
|
254
|
+
readonly remaining: number | null;
|
|
255
|
+
readonly reset: number | null;
|
|
256
|
+
/** `"general"` when RFC-9110 `RateLimit-*` headers are present; `"order_cap"` otherwise. */
|
|
257
|
+
readonly kind: RateLimitKind;
|
|
258
|
+
constructor(init: ApiErrorInit & {
|
|
259
|
+
retryAfterMs?: number | null;
|
|
260
|
+
limit?: number | null;
|
|
261
|
+
remaining?: number | null;
|
|
262
|
+
reset?: number | null;
|
|
263
|
+
kind: RateLimitKind;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** 503 — a price source or upstream dependency is temporarily unavailable. Retryable. */
|
|
267
|
+
declare class ServiceUnavailableError extends MyStarsApiError {
|
|
268
|
+
}
|
|
269
|
+
/** 500 — an unhandled server error. */
|
|
270
|
+
declare class InternalServerError extends MyStarsApiError {
|
|
271
|
+
}
|
|
272
|
+
/** The request never produced an HTTP response (DNS, connection reset, aborted, etc.). */
|
|
273
|
+
declare class NetworkError extends MyStarsApiError {
|
|
274
|
+
}
|
|
275
|
+
/** The request exceeded the configured timeout. */
|
|
276
|
+
declare class TimeoutError extends NetworkError {
|
|
277
|
+
}
|
|
278
|
+
/** A webhook payload failed signature verification (or the header was missing/malformed). */
|
|
279
|
+
declare class WebhookSignatureError extends MyStarsError {
|
|
280
|
+
}
|
|
281
|
+
/** `waitForOrder` gave up before the order reached a terminal state. */
|
|
282
|
+
declare class OrderWaitTimeoutError extends MyStarsError {
|
|
283
|
+
/** The most recent order snapshot observed before timing out. */
|
|
284
|
+
readonly lastOrder: Order;
|
|
285
|
+
constructor(lastOrder: Order, message?: string);
|
|
286
|
+
}
|
|
287
|
+
/** Parse a `Retry-After` header (delta-seconds, or an HTTP-date) into milliseconds. */
|
|
288
|
+
declare function parseRetryAfterMs(value: string | undefined, now?: number): number | null;
|
|
289
|
+
/** Map an HTTP response (status + parsed body + headers) to the appropriate typed error. */
|
|
290
|
+
declare function errorFromResponse(status: number, body: unknown, headers: Headers): MyStarsApiError;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Retry policy + backoff for transient failures.
|
|
294
|
+
*
|
|
295
|
+
* Only retries when the request is idempotency-safe AND the failure is
|
|
296
|
+
* transient. Crucially, a `createOrder` retry reuses the SAME `Idempotency-Key`
|
|
297
|
+
* (the transport guarantees this), so a retried create returns the server's
|
|
298
|
+
* idempotent replay instead of minting a duplicate deliverable.
|
|
299
|
+
*/
|
|
300
|
+
|
|
301
|
+
/** The decision context handed to {@link RetryPolicy.retryOn} / {@link defaultShouldRetry} for one failed attempt. */
|
|
302
|
+
interface RetryContext {
|
|
303
|
+
method: string;
|
|
304
|
+
path: string;
|
|
305
|
+
/** 0-based index of the attempt that just failed. */
|
|
306
|
+
attempt: number;
|
|
307
|
+
/** Whether this request is safe to replay (GET, or any request carrying an Idempotency-Key). */
|
|
308
|
+
idempotent: boolean;
|
|
309
|
+
/** The classified error from the failed attempt. */
|
|
310
|
+
error: MyStarsApiError;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Tunable retry policy passed to `MyStarsClient` (`retry`). Pass `false` instead
|
|
314
|
+
* to disable retries; omit a field to keep its default.
|
|
315
|
+
*/
|
|
316
|
+
interface RetryPolicy {
|
|
317
|
+
/** Max retries AFTER the first attempt. Default 3. */
|
|
318
|
+
maxRetries?: number;
|
|
319
|
+
/** Base backoff delay in ms. Default 500. */
|
|
320
|
+
baseDelayMs?: number;
|
|
321
|
+
/** Backoff cap in ms. Default 30_000. */
|
|
322
|
+
maxDelayMs?: number;
|
|
323
|
+
/** Jitter strategy. Default "full". */
|
|
324
|
+
jitter?: "full" | "none";
|
|
325
|
+
/** Honor a 429 `Retry-After` header as a lower bound on the delay. Default true. */
|
|
326
|
+
respectRetryAfter?: boolean;
|
|
327
|
+
/** Override the default retry classifier entirely. */
|
|
328
|
+
retryOn?: (ctx: RetryContext) => boolean;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The built-in classifier: retry idempotent requests on network/timeout/503/500,
|
|
332
|
+
* the general 429, and any other error flagged transient (e.g. a 502/504 gateway
|
|
333
|
+
* error, which `errorFromResponse` maps to a base error with `retryable: true`).
|
|
334
|
+
*/
|
|
335
|
+
declare function defaultShouldRetry(ctx: RetryContext): boolean;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The HTTP transport: URL building, auth/idempotency headers, timeout, a bounded
|
|
339
|
+
* response read, JSON parsing, and the retry loop.
|
|
340
|
+
*
|
|
341
|
+
* Conventions: trailing-slash strip, AbortController timeout, bounded read,
|
|
342
|
+
* `X-Api-Key` + `Idempotency-Key` headers, and the key is NEVER placed into any
|
|
343
|
+
* logged/intercepted object.
|
|
344
|
+
*/
|
|
345
|
+
|
|
346
|
+
/** Info passed to {@link Interceptors.onRequest} before an attempt is sent. Never contains the API key. */
|
|
347
|
+
interface RequestLogInfo {
|
|
348
|
+
method: string;
|
|
349
|
+
url: string;
|
|
350
|
+
idempotencyKey?: string;
|
|
351
|
+
}
|
|
352
|
+
/** Info passed to {@link Interceptors.onResponse} after a response is received. */
|
|
353
|
+
interface ResponseLogInfo {
|
|
354
|
+
method: string;
|
|
355
|
+
url: string;
|
|
356
|
+
status: number;
|
|
357
|
+
/** Wall-clock duration of the attempt, in ms. */
|
|
358
|
+
durationMs: number;
|
|
359
|
+
requestId?: string;
|
|
360
|
+
}
|
|
361
|
+
/** Info passed to {@link Interceptors.onRetry} just before the SDK sleeps and retries. */
|
|
362
|
+
interface RetryLogInfo {
|
|
363
|
+
method: string;
|
|
364
|
+
url: string;
|
|
365
|
+
/** 1-based index of the retry about to be made. */
|
|
366
|
+
attempt: number;
|
|
367
|
+
/** Backoff the SDK will wait before the retry, in ms. */
|
|
368
|
+
delayMs: number;
|
|
369
|
+
/** Why the retry fired, e.g. `"timeout (HTTP 0)"`. */
|
|
370
|
+
reason: string;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Observability hooks invoked around each request. All are optional, may be async
|
|
374
|
+
* (awaited inline), and NEVER receive the API key. Use them for logging/metrics —
|
|
375
|
+
* not for mutating the request.
|
|
376
|
+
*/
|
|
377
|
+
interface Interceptors {
|
|
378
|
+
onRequest?: (info: RequestLogInfo) => void | Promise<void>;
|
|
379
|
+
onResponse?: (info: ResponseLogInfo) => void | Promise<void>;
|
|
380
|
+
onRetry?: (info: RetryLogInfo) => void | Promise<void>;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Keyset (cursor) pagination over `GET /v1/orders`.
|
|
385
|
+
*
|
|
386
|
+
* `for await (const order of pager)` flattens every page; `pager.pages()` yields
|
|
387
|
+
* a page at a time; `pager.page(cursor?)` fetches a single page. The cursor is
|
|
388
|
+
* an opaque base64url token — never construct it by hand.
|
|
389
|
+
*/
|
|
390
|
+
|
|
391
|
+
/** Fetches one page given the previous page's `next_cursor` (`undefined` for the first page). */
|
|
392
|
+
type FetchOrdersPage = (cursor: string | undefined) => Promise<OrdersPage>;
|
|
393
|
+
/**
|
|
394
|
+
* Lazy, auto-advancing view over `GET /v1/orders`. Iterate it directly to stream
|
|
395
|
+
* every order, or use `pages()` / `page()` for page-at-a-time control. Returned by
|
|
396
|
+
* `client.listOrders(...)` — you rarely construct it yourself.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```ts
|
|
400
|
+
* for await (const order of client.listOrders({ status: "delivered" })) {
|
|
401
|
+
* console.log(order.order_id);
|
|
402
|
+
* }
|
|
403
|
+
* // or collect everything: const all = await client.listOrders().all();
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
declare class OrdersPager implements AsyncIterable<Order> {
|
|
407
|
+
private readonly fetchPage;
|
|
408
|
+
private readonly startCursor;
|
|
409
|
+
/**
|
|
410
|
+
* @param fetchPage - fetches a page given a cursor (the client wires this to the transport)
|
|
411
|
+
* @param startCursor - an optional cursor to begin from (resumes mid-stream)
|
|
412
|
+
*/
|
|
413
|
+
constructor(fetchPage: FetchOrdersPage, startCursor?: string);
|
|
414
|
+
/** Fetch a single page. Pass the previous page's `next_cursor` to advance. */
|
|
415
|
+
page(cursor?: string): Promise<OrdersPage>;
|
|
416
|
+
/** Yield one page at a time until `next_cursor` is null. */
|
|
417
|
+
pages(): AsyncIterableIterator<OrdersPage>;
|
|
418
|
+
/** Yield every order across all pages. */
|
|
419
|
+
[Symbol.asyncIterator](): AsyncIterator<Order>;
|
|
420
|
+
/** Collect every order across all pages into a single array. */
|
|
421
|
+
all(): Promise<Order[]>;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Reconciliation — catch terminal transitions a dropped or dead-lettered webhook
|
|
426
|
+
* missed. Webhook delivery is at-least-once and unordered, so a safety net that
|
|
427
|
+
* diffs the server's order list against your local store closes the gap.
|
|
428
|
+
*
|
|
429
|
+
* Walks `GET /v1/orders` newest-first; for each TERMINAL order your store hasn't
|
|
430
|
+
* recorded (`isKnown` returns false), it's collected (and `onMissed` fired).
|
|
431
|
+
* Stops paginating once it passes `since` (orders are newest-first).
|
|
432
|
+
*/
|
|
433
|
+
|
|
434
|
+
/** The slice of the client {@link reconcile} needs (structurally typed to avoid a cycle). */
|
|
435
|
+
interface ReconcileClient {
|
|
436
|
+
listOrders(params?: {
|
|
437
|
+
status?: OrderStatus;
|
|
438
|
+
limit?: number;
|
|
439
|
+
}): OrdersPager;
|
|
440
|
+
}
|
|
441
|
+
/** Options for {@link reconcile} — your store check plus optional status/since/page-size filters. */
|
|
442
|
+
interface ReconcileOptions {
|
|
443
|
+
/** Only reconcile a specific terminal status (else all terminal orders). */
|
|
444
|
+
status?: OrderStatus;
|
|
445
|
+
/** Stop once orders are older than this (orders are newest-first). */
|
|
446
|
+
since?: Date | string;
|
|
447
|
+
/** Page size for the underlying list. */
|
|
448
|
+
limit?: number;
|
|
449
|
+
/** Your store's check: has this terminal order already been processed? */
|
|
450
|
+
isKnown: (order: Order) => boolean | Promise<boolean>;
|
|
451
|
+
/** Fired for each missed terminal order, in newest-first order. */
|
|
452
|
+
onMissed?: (order: Order) => void | Promise<void>;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Walk the server's orders newest-first and collect TERMINAL orders your store
|
|
456
|
+
* hasn't recorded — the safety net for webhooks that were dropped or dead-lettered.
|
|
457
|
+
*
|
|
458
|
+
* @param client - anything with a `listOrders` returning an {@link OrdersPager} (a `MyStarsClient` fits)
|
|
459
|
+
* @param opts - the {@link ReconcileOptions}; `isKnown` is required, `since` bounds the scan
|
|
460
|
+
* @returns the missed terminal orders, newest-first (also delivered one-by-one to `onMissed`)
|
|
461
|
+
* @throws `MyStarsApiError` if a page fetch fails
|
|
462
|
+
* @example
|
|
463
|
+
* ```ts
|
|
464
|
+
* const missed = await client.reconcile({
|
|
465
|
+
* since: new Date(Date.now() - 24 * 3600_000),
|
|
466
|
+
* isKnown: (o) => myStore.has(o.order_id),
|
|
467
|
+
* onMissed: (o) => myStore.markTerminal(o),
|
|
468
|
+
* });
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
declare function reconcile(client: ReconcileClient, opts: ReconcileOptions): Promise<Order[]>;
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Poll an order until it reaches a terminal state (or a custom predicate / the
|
|
475
|
+
* deadline). Polling a still-`awaiting_payment` order also nudges the server's
|
|
476
|
+
* on-demand payment detection, so this doubles as a payment tracker.
|
|
477
|
+
*/
|
|
478
|
+
|
|
479
|
+
/** Polling/backoff knobs and observation hooks for {@link waitForOrder} / `client.waitForOrder`. */
|
|
480
|
+
interface WaitForOrderOptions {
|
|
481
|
+
/** First poll interval (ms). Default 2000. */
|
|
482
|
+
pollIntervalMs?: number;
|
|
483
|
+
/** Upper bound on the backed-off poll interval (ms). Default 15_000. */
|
|
484
|
+
maxPollIntervalMs?: number;
|
|
485
|
+
/** Multiplier applied to the interval after each poll. Default 1.5. */
|
|
486
|
+
backoffFactor?: number;
|
|
487
|
+
/** Total time to wait before throwing `OrderWaitTimeoutError` (ms). Default 30 min. */
|
|
488
|
+
maxWaitMs?: number;
|
|
489
|
+
/** Apply jitter to the poll interval. Default "full". */
|
|
490
|
+
jitter?: "full" | "none";
|
|
491
|
+
/** Resolve when this returns true. Default: the order is terminal. */
|
|
492
|
+
until?: (order: Order) => boolean;
|
|
493
|
+
/** Called whenever the observed status changes (including the first observation). */
|
|
494
|
+
onUpdate?: (order: Order) => void;
|
|
495
|
+
signal?: AbortSignal;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* `MyStarsClient` — the typed entry point to the MyStars FaaS API.
|
|
500
|
+
*
|
|
501
|
+
* Wraps the 8 public `/v1` endpoints with typed requests/responses, automatic
|
|
502
|
+
* retries (honoring `Retry-After`), automatic `Idempotency-Key` generation +
|
|
503
|
+
* safe reuse on retry, keyset pagination, and order tracking.
|
|
504
|
+
*/
|
|
505
|
+
|
|
506
|
+
/** Base URL of the production B2B edge (`api.mystars.tg`). */
|
|
507
|
+
declare const PRODUCTION_BASE_URL = "https://api.mystars.tg/v1";
|
|
508
|
+
/** Configuration for {@link MyStarsClient}. Only `apiKey` is required. */
|
|
509
|
+
interface MyStarsClientOptions {
|
|
510
|
+
/** Your tenant API key (`faas_…`). Sent as the `X-Api-Key` header. */
|
|
511
|
+
apiKey: string;
|
|
512
|
+
/** Full base URL incl. `/v1`. Defaults to production (`api.mystars.tg`). */
|
|
513
|
+
baseUrl?: string;
|
|
514
|
+
/** Injected fetch. Defaults to the global `fetch`. */
|
|
515
|
+
fetch?: typeof fetch;
|
|
516
|
+
/** Per-request timeout in ms. Default 30_000. */
|
|
517
|
+
timeoutMs?: number;
|
|
518
|
+
/** Retry policy, or `false` to disable retries. */
|
|
519
|
+
retry?: RetryPolicy | false;
|
|
520
|
+
/** Generates the `Idempotency-Key` for `createOrder` when the caller omits one. Default uuid v4. */
|
|
521
|
+
idempotencyKeyFactory?: () => string;
|
|
522
|
+
/** Sent as `User-Agent` (Node only; browsers ignore it). */
|
|
523
|
+
userAgent?: string;
|
|
524
|
+
/** Observability hooks. Never receive the API key. */
|
|
525
|
+
interceptors?: Interceptors;
|
|
526
|
+
/** Test injectables. */
|
|
527
|
+
now?: () => number;
|
|
528
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
529
|
+
random?: () => number;
|
|
530
|
+
}
|
|
531
|
+
/** Per-call options common to every request — currently just an `AbortSignal`. */
|
|
532
|
+
interface RequestOptions {
|
|
533
|
+
signal?: AbortSignal;
|
|
534
|
+
}
|
|
535
|
+
/** Options for {@link MyStarsClient.createOrder} — a {@link RequestOptions} plus an optional stable key. */
|
|
536
|
+
interface CreateOrderOptions extends RequestOptions {
|
|
537
|
+
/** Reuse a specific idempotency key (e.g. derived from your own order id). */
|
|
538
|
+
idempotencyKey?: string;
|
|
539
|
+
}
|
|
540
|
+
/** Discriminated input for {@link MyStarsClient.getPricing} — sized by `quantity` (stars) or `months` (premium). */
|
|
541
|
+
type PricingParams = {
|
|
542
|
+
type: "stars";
|
|
543
|
+
quantity: number;
|
|
544
|
+
payment_currency?: Currency;
|
|
545
|
+
} | {
|
|
546
|
+
type: "premium";
|
|
547
|
+
months: number;
|
|
548
|
+
payment_currency?: Currency;
|
|
549
|
+
};
|
|
550
|
+
/** Discriminated input for {@link MyStarsClient.checkRecipient} (premium may include the intended `months`). */
|
|
551
|
+
type CheckRecipientParams = {
|
|
552
|
+
type: "stars";
|
|
553
|
+
recipient: {
|
|
554
|
+
username: string;
|
|
555
|
+
};
|
|
556
|
+
} | {
|
|
557
|
+
type: "premium";
|
|
558
|
+
recipient: {
|
|
559
|
+
username: string;
|
|
560
|
+
};
|
|
561
|
+
months?: number;
|
|
562
|
+
};
|
|
563
|
+
/** Discriminated input for {@link MyStarsClient.createOrder} — recipient + sizing + optional currency/callback. */
|
|
564
|
+
type CreateOrderParams = {
|
|
565
|
+
type: "stars";
|
|
566
|
+
recipient: {
|
|
567
|
+
username: string;
|
|
568
|
+
};
|
|
569
|
+
quantity: number;
|
|
570
|
+
payment_currency?: Currency;
|
|
571
|
+
callback_url?: string;
|
|
572
|
+
} | {
|
|
573
|
+
type: "premium";
|
|
574
|
+
recipient: {
|
|
575
|
+
username: string;
|
|
576
|
+
};
|
|
577
|
+
months: number;
|
|
578
|
+
payment_currency?: Currency;
|
|
579
|
+
callback_url?: string;
|
|
580
|
+
};
|
|
581
|
+
/** Filters for {@link MyStarsClient.listOrders} — status, page size, and a starting cursor. */
|
|
582
|
+
interface ListOrdersParams {
|
|
583
|
+
status?: OrderStatus;
|
|
584
|
+
/** 1-100; the server caps at 100 and defaults to 50. */
|
|
585
|
+
limit?: number;
|
|
586
|
+
cursor?: string;
|
|
587
|
+
}
|
|
588
|
+
/** The typed entry point to the MyStars FaaS `/v1` API. See the module overview for the full flow. */
|
|
589
|
+
declare class MyStarsClient {
|
|
590
|
+
private readonly transport;
|
|
591
|
+
private readonly idempotencyKeyFactory;
|
|
592
|
+
private readonly now;
|
|
593
|
+
private readonly sleep;
|
|
594
|
+
private readonly random;
|
|
595
|
+
/**
|
|
596
|
+
* @param opts - the {@link MyStarsClientOptions} (at minimum `apiKey`). Prefer the
|
|
597
|
+
* {@link MyStarsClient.production} factory.
|
|
598
|
+
* @throws `Error` if `apiKey` is missing, or no `fetch` is available (Node <18 — pass `fetch` explicitly)
|
|
599
|
+
*/
|
|
600
|
+
constructor(opts: MyStarsClientOptions);
|
|
601
|
+
/**
|
|
602
|
+
* Build a client pointed at production (`api.mystars.tg`).
|
|
603
|
+
*
|
|
604
|
+
* @param apiKey - your tenant `faas_…` API key
|
|
605
|
+
* @param opts - any other {@link MyStarsClientOptions} except `apiKey`/`baseUrl`
|
|
606
|
+
* @returns a configured client
|
|
607
|
+
* @example
|
|
608
|
+
* ```ts
|
|
609
|
+
* const client = MyStarsClient.production(process.env.MYSTARS_API_KEY!);
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
static production(apiKey: string, opts?: Omit<MyStarsClientOptions, "apiKey" | "baseUrl">): MyStarsClient;
|
|
613
|
+
/**
|
|
614
|
+
* `GET /v1/currencies` — the supported on-chain payment currencies.
|
|
615
|
+
*
|
|
616
|
+
* @param opts - optional `signal` to abort the request
|
|
617
|
+
* @returns the supported {@link CurrencyInfo} list
|
|
618
|
+
* @throws `MyStarsApiError` on an API/network failure
|
|
619
|
+
*/
|
|
620
|
+
listCurrencies(opts?: RequestOptions): Promise<CurrencyInfo[]>;
|
|
621
|
+
/**
|
|
622
|
+
* `GET /v1/products` — the orderable product catalog (price-free).
|
|
623
|
+
*
|
|
624
|
+
* @param opts - optional `signal` to abort the request
|
|
625
|
+
* @returns the {@link Product} catalog (buyable shapes + limits)
|
|
626
|
+
* @throws `MyStarsApiError` on an API/network failure
|
|
627
|
+
*/
|
|
628
|
+
listProducts(opts?: RequestOptions): Promise<Product[]>;
|
|
629
|
+
/**
|
|
630
|
+
* `GET /v1/pricing` — quote the all-in price for an item. Probe-rate-limited (30/min).
|
|
631
|
+
*
|
|
632
|
+
* @param params - `{ type, quantity | months, payment_currency? }`; `payment_currency` defaults server-side
|
|
633
|
+
* @param opts - optional `signal` to abort the request
|
|
634
|
+
* @returns the {@link PricingQuote} — `amount` is the all-in total in `currency`; `fee` is itemized for `usdt_ton`
|
|
635
|
+
* @throws `MyStarsValidationError` if `quantity`/`months` is out of range
|
|
636
|
+
* @throws `RateLimitError` (429) if the probe limit is exceeded
|
|
637
|
+
* @throws `ServiceUnavailableError` (503) if a price source is temporarily down
|
|
638
|
+
* @example
|
|
639
|
+
* ```ts
|
|
640
|
+
* const q = await client.getPricing({ type: "stars", quantity: 500, payment_currency: "ton" });
|
|
641
|
+
* console.log(`pay ${q.amount} ${q.currency}`);
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
644
|
+
getPricing(params: PricingParams, opts?: RequestOptions): Promise<PricingQuote>;
|
|
645
|
+
/**
|
|
646
|
+
* `POST /v1/recipients/check` — resolve a `@username` and check eligibility before ordering.
|
|
647
|
+
*
|
|
648
|
+
* @param params - `{ type, recipient: { username }, months? }`; the username is canonicalized (strip `@`, lowercase)
|
|
649
|
+
* @param opts - optional `signal` to abort the request
|
|
650
|
+
* @returns the {@link RecipientCheck} — `resolved`/`eligible`, the display `recipient_name`, and a `reason` when ineligible
|
|
651
|
+
* @throws `MyStarsValidationError` if the username is malformed
|
|
652
|
+
* @throws `MyStarsApiError` on an API/network failure
|
|
653
|
+
*/
|
|
654
|
+
checkRecipient(params: CheckRecipientParams, opts?: RequestOptions): Promise<RecipientCheck>;
|
|
655
|
+
/**
|
|
656
|
+
* `POST /v1/orders` — create an order. An `Idempotency-Key` is sent once and
|
|
657
|
+
* reused across this call's retries, so a retried create returns the idempotent
|
|
658
|
+
* replay (`replayed: true`) instead of a duplicate.
|
|
659
|
+
*
|
|
660
|
+
* MONEY SAFETY — supply a STABLE key derived from YOUR OWN order id
|
|
661
|
+
* (`{ idempotencyKey: \`order-\${myOrderId}\` }`). The auto-generated uuid only
|
|
662
|
+
* dedupes WITHIN a single `createOrder` call's internal retries; a brand-new
|
|
663
|
+
* call (e.g. your process crashed and re-ran) mints a fresh uuid and can create
|
|
664
|
+
* a SECOND order for the same intent. A caller-stable key makes the create
|
|
665
|
+
* idempotent across process restarts and at-least-once job runners.
|
|
666
|
+
*
|
|
667
|
+
* On failure the thrown {@link MyStarsApiError} carries the `idempotencyKey`
|
|
668
|
+
* that was used — when you can't tell whether the order was created
|
|
669
|
+
* server-side, retry with that exact key (`{ idempotencyKey: err.idempotencyKey }`)
|
|
670
|
+
* to get the idempotent replay rather than a duplicate.
|
|
671
|
+
*
|
|
672
|
+
* @param params - `{ type, recipient: { username }, quantity | months, payment_currency?, callback_url? }`
|
|
673
|
+
* @param opts - optional caller-stable `idempotencyKey` (recommended) and `signal`
|
|
674
|
+
* @returns the {@link CreateOrderResult} — `payment` instruction + `expires_at`; `replayed` is `true` on a 200 idempotent replay
|
|
675
|
+
* @throws `MyStarsValidationError` if inputs are invalid
|
|
676
|
+
* @throws `RecipientIneligibleError` (422) if the recipient cannot receive the item (no order created)
|
|
677
|
+
* @throws `IdempotencyConflictError` (409) if the same key is reused with a different body
|
|
678
|
+
* @throws `MyStarsApiError` on any other API failure (carries the `idempotencyKey` for safe retry)
|
|
679
|
+
* @example
|
|
680
|
+
* ```ts
|
|
681
|
+
* const order = await client.createOrder(
|
|
682
|
+
* { type: "stars", recipient: { username: "durov" }, quantity: 100 },
|
|
683
|
+
* { idempotencyKey: `order-${myOrderId}` },
|
|
684
|
+
* );
|
|
685
|
+
* // pay order.payment.amount → order.payment.pay_to_address, comment = order.payment.memo
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
createOrder(params: CreateOrderParams, opts?: CreateOrderOptions): Promise<CreateOrderResult>;
|
|
689
|
+
/**
|
|
690
|
+
* `GET /v1/orders/:id` — fetch one order (tenant-scoped). Polling an
|
|
691
|
+
* `awaiting_payment` order also nudges the server's on-demand payment detection.
|
|
692
|
+
*
|
|
693
|
+
* @param orderId - the order UUID
|
|
694
|
+
* @param opts - optional `signal` to abort the request
|
|
695
|
+
* @returns the {@link Order}
|
|
696
|
+
* @throws `NotFoundError` (404) if no such order exists for this tenant
|
|
697
|
+
* @throws `MyStarsApiError` on any other API failure
|
|
698
|
+
*/
|
|
699
|
+
getOrder(orderId: string, opts?: RequestOptions): Promise<Order>;
|
|
700
|
+
/**
|
|
701
|
+
* `POST /v1/orders/:id/cancel` — cancel an `awaiting_payment` order.
|
|
702
|
+
*
|
|
703
|
+
* @param orderId - the order UUID
|
|
704
|
+
* @param opts - optional `signal` to abort the request
|
|
705
|
+
* @returns `{ order_id, status: "cancelled" }`
|
|
706
|
+
* @throws `OrderNotCancellableError` (409) if the order is no longer `awaiting_payment` (a retry after a lost success can also surface this even though the cancel committed — re-check with `getOrder`)
|
|
707
|
+
* @throws `MyStarsApiError` on any other API failure
|
|
708
|
+
*/
|
|
709
|
+
cancelOrder(orderId: string, opts?: RequestOptions): Promise<{
|
|
710
|
+
order_id: string;
|
|
711
|
+
status: "cancelled";
|
|
712
|
+
}>;
|
|
713
|
+
private listOrdersPage;
|
|
714
|
+
/**
|
|
715
|
+
* `GET /v1/orders` — an auto-paginating view: `for await (const order of client.listOrders())`.
|
|
716
|
+
* The pager owns the cursor; `params.cursor` (if given) is the starting page.
|
|
717
|
+
*
|
|
718
|
+
* @param params - optional `status` filter, `limit` (1-100), and a starting `cursor`
|
|
719
|
+
* @param opts - optional `signal` to abort the underlying page fetches
|
|
720
|
+
* @returns an {@link OrdersPager} (async-iterable; `.pages()` / `.page()` / `.all()`)
|
|
721
|
+
* @example
|
|
722
|
+
* ```ts
|
|
723
|
+
* for await (const order of client.listOrders({ status: "delivered" })) {
|
|
724
|
+
* console.log(order.order_id);
|
|
725
|
+
* }
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
728
|
+
listOrders(params?: ListOrdersParams, opts?: RequestOptions): OrdersPager;
|
|
729
|
+
/**
|
|
730
|
+
* Poll `GET /v1/orders/:id` until the order is terminal (or the deadline).
|
|
731
|
+
*
|
|
732
|
+
* @param orderId - the order UUID
|
|
733
|
+
* @param options - the {@link WaitForOrderOptions} (intervals, deadline, `until`, `onUpdate`, `signal`)
|
|
734
|
+
* @returns the final {@link Order} once terminal
|
|
735
|
+
* @throws `OrderWaitTimeoutError` if `maxWaitMs` elapses first (carries the last snapshot)
|
|
736
|
+
* @throws `MyStarsApiError` if a poll fails non-transiently
|
|
737
|
+
* @example
|
|
738
|
+
* ```ts
|
|
739
|
+
* const final = await client.waitForOrder(order.order_id, { onUpdate: (o) => console.log(o.status) });
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
waitForOrder(orderId: string, options?: WaitForOrderOptions): Promise<Order>;
|
|
743
|
+
/**
|
|
744
|
+
* Diff the server's orders against your local store to catch webhook-missed terminal transitions.
|
|
745
|
+
*
|
|
746
|
+
* @param options - the {@link ReconcileOptions}; `isKnown` is required, `since` bounds the scan
|
|
747
|
+
* @returns the missed terminal {@link Order}s, newest-first
|
|
748
|
+
* @throws `MyStarsApiError` if a page fetch fails
|
|
749
|
+
*/
|
|
750
|
+
reconcile(options: ReconcileOptions): Promise<Order[]>;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Webhook signature verification.
|
|
755
|
+
*
|
|
756
|
+
* Reimplements the server's webhook signing byte-for-byte: the `X-Faas-Signature`
|
|
757
|
+
* header is the lowercase-hex HMAC-SHA256 of the RAW request body under the tenant's
|
|
758
|
+
* webhook secret — no timestamp. During a 24h secret rotation the header is two
|
|
759
|
+
* comma-joined signatures (`"<current>,<previous>"`); we split on `,` and
|
|
760
|
+
* constant-time-compare each, so verification holds with EITHER secret.
|
|
761
|
+
*
|
|
762
|
+
* Universal: prefers Web Crypto (`globalThis.crypto.subtle`) so it runs in Deno,
|
|
763
|
+
* Bun, Cloudflare Workers, and browsers; on default Node 18 (where the global is
|
|
764
|
+
* absent) it falls back to `node:crypto`'s `webcrypto`. The verifier is therefore async.
|
|
765
|
+
*/
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Verify an `X-Faas-Signature` header against the raw webhook body.
|
|
769
|
+
* Handles the single-signature and the `"current,previous"` rotation forms.
|
|
770
|
+
*/
|
|
771
|
+
declare function verifyWebhookSignature(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string): Promise<boolean>;
|
|
772
|
+
/**
|
|
773
|
+
* Verify the signature, then JSON-parse the body into a typed {@link WebhookEvent}.
|
|
774
|
+
* Throws {@link WebhookSignatureError} on a bad/missing signature or unparseable body.
|
|
775
|
+
*
|
|
776
|
+
* IMPORTANT: pass the RAW request bytes/string (verify before any framework
|
|
777
|
+
* re-serializes the JSON), and dedup on `event.order_id` — delivery is
|
|
778
|
+
* at-least-once and unordered.
|
|
779
|
+
*/
|
|
780
|
+
declare function constructEvent(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string): Promise<WebhookEvent>;
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Drop-in webhook handlers for Express and Fastify.
|
|
784
|
+
*
|
|
785
|
+
* Both verify the `X-Faas-Signature` over the RAW body, parse the event, hand it
|
|
786
|
+
* to your `onEvent`, and reply `2xx` fast (the server needs a 2xx within 5s).
|
|
787
|
+
* They are structurally typed (no `express`/`fastify` runtime dependency), so the
|
|
788
|
+
* SDK stays dependency-free.
|
|
789
|
+
*
|
|
790
|
+
* Dedup is YOUR job — delivery is at-least-once and unordered; key on
|
|
791
|
+
* `event.order_id` (+ `status`).
|
|
792
|
+
*/
|
|
793
|
+
|
|
794
|
+
interface ExpressLikeReq {
|
|
795
|
+
headers: Record<string, string | string[] | undefined>;
|
|
796
|
+
body?: unknown;
|
|
797
|
+
rawBody?: unknown;
|
|
798
|
+
}
|
|
799
|
+
interface ExpressLikeRes {
|
|
800
|
+
status(code: number): ExpressLikeRes;
|
|
801
|
+
send(body?: unknown): unknown;
|
|
802
|
+
}
|
|
803
|
+
type ExpressLikeHandler = (req: ExpressLikeReq, res: ExpressLikeRes) => Promise<void>;
|
|
804
|
+
/** Options for {@link expressWebhook} / {@link fastifyWebhook} — the secret, the event handler, and an error hook. */
|
|
805
|
+
interface WebhookMiddlewareOptions<Req = unknown> {
|
|
806
|
+
/** The tenant webhook secret, or a function resolving it per-request (e.g. multi-tenant routing). */
|
|
807
|
+
secret: string | ((req: Req) => string | Promise<string>);
|
|
808
|
+
/** Called with the verified, parsed event. Keep it fast; offload heavy work to a queue. */
|
|
809
|
+
onEvent: (event: WebhookEvent, ctx: {
|
|
810
|
+
rawBody: string;
|
|
811
|
+
req: Req;
|
|
812
|
+
}) => void | Promise<void>;
|
|
813
|
+
/** Optional hook for signature/handler errors (after the response is sent). */
|
|
814
|
+
onError?: (err: unknown, req: Req) => void;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Express handler. REQUIRES the raw body — mount with `express.raw({ type: "*\/*" })`
|
|
818
|
+
* (or any raw-body parser) on the webhook route so `req.body`/`req.rawBody` is a
|
|
819
|
+
* Buffer/string, NOT a pre-parsed object.
|
|
820
|
+
*/
|
|
821
|
+
declare function expressWebhook(opts: WebhookMiddlewareOptions<ExpressLikeReq>): ExpressLikeHandler;
|
|
822
|
+
interface FastifyLikeReq {
|
|
823
|
+
headers: Record<string, string | string[] | undefined>;
|
|
824
|
+
body?: unknown;
|
|
825
|
+
rawBody?: unknown;
|
|
826
|
+
}
|
|
827
|
+
interface FastifyLikeReply {
|
|
828
|
+
code(statusCode: number): FastifyLikeReply;
|
|
829
|
+
send(payload?: unknown): unknown;
|
|
830
|
+
}
|
|
831
|
+
type FastifyLikeHandler = (req: FastifyLikeReq, reply: FastifyLikeReply) => Promise<void>;
|
|
832
|
+
/**
|
|
833
|
+
* Fastify handler. REQUIRES the raw body — register a content-type parser that
|
|
834
|
+
* keeps the Buffer (e.g. `addContentTypeParser("application/json", { parseAs: "buffer" }, (req, body, done) => done(null, body))`)
|
|
835
|
+
* so `req.body` is a Buffer/string, not a pre-parsed object.
|
|
836
|
+
*/
|
|
837
|
+
declare function fastifyWebhook(opts: WebhookMiddlewareOptions<FastifyLikeReq>): FastifyLikeHandler;
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Retail-markup calculator.
|
|
841
|
+
*
|
|
842
|
+
* Takes our WHOLESALE quote (what you pay us) and computes the price to charge
|
|
843
|
+
* your end-customer after adding your OWN retail margin — and, optionally,
|
|
844
|
+
* passing our processing fee straight through to the customer.
|
|
845
|
+
*
|
|
846
|
+
* Money is handled to the same grid the server uses: USDT to whole cents via the
|
|
847
|
+
* exact two-stage cent-ceil (`ceilUsdToCents`), and TON to the 0.0001-GRAM grid.
|
|
848
|
+
* The cross-language `markup-vectors.json` fixture pins this so the TS and Python
|
|
849
|
+
* SDKs produce identical retail prices.
|
|
850
|
+
*
|
|
851
|
+
* NOTE: our wholesale markup is set server-side and is redacted — this module
|
|
852
|
+
* never sees it. The margin here is purely YOUR retail margin on top of the
|
|
853
|
+
* quoted wholesale amount.
|
|
854
|
+
*/
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Ceil a USD(T) amount to whole cents WITHOUT IEEE-754 drift — snap to integer
|
|
858
|
+
* micro-USDT first, then ceil to the cent grid. Byte-identical to the server's
|
|
859
|
+
* `ceilUsdToCents` (web checkout + FaaS `/v1/pricing` land on the same cent).
|
|
860
|
+
*
|
|
861
|
+
* @example ceilUsdToCents(0.06) // 0.06 (naive Math.ceil(x*100)/100 returns 0.07)
|
|
862
|
+
* @example ceilUsdToCents(3.3461) // 3.35
|
|
863
|
+
*/
|
|
864
|
+
declare function ceilUsdToCents(usd: number): number;
|
|
865
|
+
/** Ceil a TON amount to the 0.0001-GRAM grid (snap to integer nanoTON first). */
|
|
866
|
+
declare function ceilTonTo4dp(ton: number): number;
|
|
867
|
+
/** The wholesale quote a retail markup is applied to (a `PricingQuote` or a `PaymentInstruction` both fit). */
|
|
868
|
+
interface MarkupInput {
|
|
869
|
+
amount: string;
|
|
870
|
+
currency: Currency;
|
|
871
|
+
fee: FeeBreakdown | null;
|
|
872
|
+
}
|
|
873
|
+
/** Your retail-margin configuration for {@link applyRetailMarkup}. */
|
|
874
|
+
interface RetailMarkupConfig {
|
|
875
|
+
/** Your retail margin, in percent, applied to the goods value (e.g. 12.5 for +12.5%). */
|
|
876
|
+
marginPct: number;
|
|
877
|
+
/**
|
|
878
|
+
* When true (default), our processing fee (`usdt_ton` only) is added to the
|
|
879
|
+
* customer total as a separate line — the customer pays it, you remit it to us,
|
|
880
|
+
* and it doesn't eat your margin. When false, you absorb the fee out of your margin.
|
|
881
|
+
*/
|
|
882
|
+
passThroughProcessingFee?: boolean;
|
|
883
|
+
}
|
|
884
|
+
/** One labelled line of a {@link RetailQuote} breakdown (label + decimal-string amount). */
|
|
885
|
+
interface RetailLineItem {
|
|
886
|
+
label: string;
|
|
887
|
+
amount: string;
|
|
888
|
+
}
|
|
889
|
+
/** The customer-facing breakdown returned by {@link applyRetailMarkup}. All amounts are decimal strings. */
|
|
890
|
+
interface RetailQuote {
|
|
891
|
+
currency: Currency;
|
|
892
|
+
/** What you pay us (the wholesale quote amount). */
|
|
893
|
+
wholesaleAmount: string;
|
|
894
|
+
marginPct: number;
|
|
895
|
+
/** The base goods value before your margin (the fee's `subtotal` for usdt_ton, else `amount`). */
|
|
896
|
+
goods: string;
|
|
897
|
+
/** Your added margin (subtotal − goods). */
|
|
898
|
+
markup: string;
|
|
899
|
+
/** Marked-up goods (goods + markup). */
|
|
900
|
+
subtotal: string;
|
|
901
|
+
/** Passed-through processing fee (usdt_ton + passThrough), else "0". */
|
|
902
|
+
processingFee: string;
|
|
903
|
+
/** What to charge your customer (subtotal + processingFee). */
|
|
904
|
+
total: string;
|
|
905
|
+
/** Your gross margin on the sale (total − wholesaleAmount). */
|
|
906
|
+
profit: string;
|
|
907
|
+
lineItems: RetailLineItem[];
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Apply your retail margin to a wholesale quote and return an itemized
|
|
911
|
+
* customer-facing breakdown.
|
|
912
|
+
*/
|
|
913
|
+
declare function applyRetailMarkup(input: MarkupInput, config: RetailMarkupConfig): RetailQuote;
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Non-custodial invoice builder.
|
|
917
|
+
*
|
|
918
|
+
* Turns an order's `payment` block into things you can pay with: smallest-unit
|
|
919
|
+
* amounts, a `ton://transfer` deeplink, a Tonkeeper link, a QR payload, and TON
|
|
920
|
+
* Connect message(s). This module holds NO keys and signs nothing — feed the
|
|
921
|
+
* output to a wallet / TON Connect, or to `@mystars-tg/faas-wallet` to broadcast.
|
|
922
|
+
*/
|
|
923
|
+
|
|
924
|
+
/** Message value (nanoTON) attached to a USDT jetton transfer to cover its gas. */
|
|
925
|
+
declare const JETTON_TRANSFER_GAS_NANO = "50000000";
|
|
926
|
+
/**
|
|
927
|
+
* Convert a non-negative decimal string to integer smallest units (half-up),
|
|
928
|
+
* without IEEE-754 drift. A leading `-` is rejected — a payment amount is never
|
|
929
|
+
* negative, and a signed value would build a nonsensical (or zero) transfer.
|
|
930
|
+
*/
|
|
931
|
+
declare function decimalToUnits(amount: string, decimals: number): bigint;
|
|
932
|
+
/** Decimal TON string → nanoTON. */
|
|
933
|
+
declare function toNano(amount: string): bigint;
|
|
934
|
+
/** Decimal USDT string → micro-USDT. */
|
|
935
|
+
declare function toMicro(amount: string): bigint;
|
|
936
|
+
/** One TON Connect `messages[]` entry — feed to `tonConnectUI.sendTransaction({ messages })`. */
|
|
937
|
+
interface TonConnectMessage {
|
|
938
|
+
address: string;
|
|
939
|
+
/** nanoTON, as a string (TON Connect's expected form). */
|
|
940
|
+
amount: string;
|
|
941
|
+
/** base64 BoC payload. */
|
|
942
|
+
payload?: string;
|
|
943
|
+
}
|
|
944
|
+
/** Options for the invoice builders — only needed to produce a signable USDT (jetton) message. */
|
|
945
|
+
interface BuildInvoiceOptions {
|
|
946
|
+
/** The payer's wallet address (raw or friendly) — required to build a USDT jetton message. */
|
|
947
|
+
senderAddress?: string;
|
|
948
|
+
/** The payer's OWN USDT jetton wallet address — required to build a USDT TON Connect message. */
|
|
949
|
+
jettonWalletAddress?: string;
|
|
950
|
+
/** Validity window for the TON Connect transaction, in seconds. Default 600. */
|
|
951
|
+
validForSeconds?: number;
|
|
952
|
+
/** Stamp for `valid_until` (epoch ms). Pass `Date.now()` — kept injectable for determinism. */
|
|
953
|
+
now?: number;
|
|
954
|
+
}
|
|
955
|
+
/** The aggregated payable artifacts for an order, returned by {@link buildPaymentRequest}. */
|
|
956
|
+
interface PaymentRequest {
|
|
957
|
+
currency: PaymentInstruction["currency"];
|
|
958
|
+
payToAddress: string;
|
|
959
|
+
memo: string;
|
|
960
|
+
amountUnits: "ton" | "usdt";
|
|
961
|
+
/** nanoTON (ton) or micro-USDT (usdt_ton), as a string. */
|
|
962
|
+
amountSmallestUnit: string;
|
|
963
|
+
/** `ton://transfer/...` — TON only (a USDT jetton transfer has no plain deeplink). */
|
|
964
|
+
tonDeeplink?: string;
|
|
965
|
+
/** `https://app.tonkeeper.com/transfer/...` — TON only. */
|
|
966
|
+
tonkeeperLink?: string;
|
|
967
|
+
/** A URI to render as a QR code — TON only. */
|
|
968
|
+
qrPayload?: string;
|
|
969
|
+
/** TON Connect `messages` array (USDT requires `senderAddress` + `jettonWalletAddress`). */
|
|
970
|
+
tonConnect: TonConnectMessage[];
|
|
971
|
+
/** Set when a field couldn't be produced (e.g. USDT without a resolved jetton wallet). */
|
|
972
|
+
note?: string;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Build TON Connect message(s) for the payment. Throws for a USDT payment unless
|
|
976
|
+
* `senderAddress` + `jettonWalletAddress` are supplied (a jetton message must be
|
|
977
|
+
* sent to the payer's own jetton wallet).
|
|
978
|
+
*/
|
|
979
|
+
declare function buildTonConnectMessages(payment: PaymentInstruction, opts?: BuildInvoiceOptions): TonConnectMessage[];
|
|
980
|
+
/** Build a `ton://transfer` deeplink. TON only — throws for USDT. */
|
|
981
|
+
declare function buildTonDeeplink(payment: PaymentInstruction): string;
|
|
982
|
+
/**
|
|
983
|
+
* Aggregate everything you can pay the order with. Best-effort: TON yields the
|
|
984
|
+
* full set; USDT yields the smallest-unit amount + TON Connect message only when
|
|
985
|
+
* `senderAddress` + `jettonWalletAddress` are supplied (otherwise `note` explains).
|
|
986
|
+
*/
|
|
987
|
+
declare function buildPaymentRequest(payment: PaymentInstruction, opts?: BuildInvoiceOptions): PaymentRequest;
|
|
988
|
+
|
|
989
|
+
/** Build the op-0 text-comment payload for `comment`, as a base64 BoC. */
|
|
990
|
+
declare function buildCommentPayload(comment: string): string;
|
|
991
|
+
|
|
992
|
+
/** `forward_ton_amount` carried inside the transfer (0 — the memo still survives in internal_transfer). */
|
|
993
|
+
declare const FORWARD_TON_AMOUNT_NANO: bigint;
|
|
994
|
+
/** Jetton transfer opcode (TEP-74). */
|
|
995
|
+
declare const JETTON_TRANSFER_OP = 260734629;
|
|
996
|
+
/** Parse a TON address (friendly base64url or raw `wc:hex`) into workchain + 32-byte hash. Throws on a bad checksum/shape. */
|
|
997
|
+
declare function parseTonAddress(address: string): {
|
|
998
|
+
workchain: number;
|
|
999
|
+
hash: Uint8Array;
|
|
1000
|
+
};
|
|
1001
|
+
/**
|
|
1002
|
+
* Build the TEP-74 jetton transfer BoC payload (base64).
|
|
1003
|
+
*
|
|
1004
|
+
* @param amountMicro - jetton amount in micro-units (e.g. "4990000" for 4.99 USDT)
|
|
1005
|
+
* @param destination - the recipient OWNER address (the FaaS `pay_to_address`), raw or friendly
|
|
1006
|
+
* @param sender - the payer's wallet address (`response_destination`), raw or friendly
|
|
1007
|
+
* @param memo - the bare order UUID (op-0 comment in the forward payload)
|
|
1008
|
+
*/
|
|
1009
|
+
declare function buildJettonTransferPayload(amountMicro: string | bigint, destination: string, sender: string, memo: string): string;
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Lightweight client-side validation that mirrors the server's documented
|
|
1013
|
+
* constraints (and the `GET /v1/products` catalog), so common mistakes fail
|
|
1014
|
+
* fast without a round trip. These constants track the pinned CONTRACT_VERSION.
|
|
1015
|
+
*/
|
|
1016
|
+
|
|
1017
|
+
/** Minimum Stars quantity a single order can buy (inclusive). */
|
|
1018
|
+
declare const STARS_MIN_QUANTITY = 50;
|
|
1019
|
+
/** Maximum Stars quantity a single order can buy (inclusive). */
|
|
1020
|
+
declare const STARS_MAX_QUANTITY = 1000000;
|
|
1021
|
+
/** The allowed Telegram Premium subscription lengths, in months. */
|
|
1022
|
+
declare const PREMIUM_MONTHS: readonly number[];
|
|
1023
|
+
/** Thrown for invalid input caught before any HTTP request is made. */
|
|
1024
|
+
declare class MyStarsValidationError extends MyStarsError {
|
|
1025
|
+
}
|
|
1026
|
+
/** Canonicalize a Telegram username the same way the server does: strip a leading `@`, lowercase. */
|
|
1027
|
+
declare function canonicalUsername(input: string): string;
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* The MyStars FaaS API contract version this SDK was built and verified against.
|
|
1031
|
+
*
|
|
1032
|
+
* Bumped in lockstep with the SDK's contract fixtures; CI fails the build on drift.
|
|
1033
|
+
*/
|
|
1034
|
+
declare const CONTRACT_VERSION = "1.9.0";
|
|
1035
|
+
/** This SDK's own semantic version (decoupled from the API contract version). */
|
|
1036
|
+
declare const SDK_VERSION = "0.1.2";
|
|
1037
|
+
|
|
1038
|
+
export { BadRequestError, type BuildInvoiceOptions, CANCELLABLE_STATUSES, CONTRACT_VERSION, type CheckRecipientParams, ConflictError, type CreateOrderOptions, type CreateOrderParams, type CreateOrderResult, type Currency, type CurrencyInfo, FORWARD_TON_AMOUNT_NANO, type FailureReason, type FeeBreakdown, type FetchOrdersPage, ForbiddenError, INITIAL_ORDER_STATUS, IdempotencyConflictError, type Interceptors, InternalServerError, JETTON_TRANSFER_GAS_NANO, JETTON_TRANSFER_OP, type ListOrdersParams, type MarkupInput, MyStarsApiError, MyStarsClient, type MyStarsClientOptions, MyStarsError, MyStarsValidationError, NetworkError, NotFoundError, type Order, OrderNotCancellableError, type OrderStatus, type OrderType, OrderWaitTimeoutError, type OrdersPage, OrdersPager, PREMIUM_MONTHS, PRODUCTION_BASE_URL, type Parameter, type PaymentInstruction, type PaymentRequest, type PricingParams, type PricingQuote, type Product, RateLimitError, type RateLimitKind, type RecipientCheck, type RecipientCheckReason, RecipientIneligibleError, type ReconcileClient, type ReconcileOptions, type RequestLogInfo, type RequestOptions, type ResponseLogInfo, type RetailLineItem, type RetailMarkupConfig, type RetailQuote, type RetryContext, type RetryLogInfo, type RetryPolicy, SDK_VERSION, STARS_MAX_QUANTITY, STARS_MIN_QUANTITY, ServiceUnavailableError, TERMINAL_STATUSES, type TerminalStatus, TimeoutError, type TonConnectMessage, UnauthorizedError, WEBHOOK_TERMINAL, type WaitForOrderOptions, type WebhookEvent, type WebhookMiddlewareOptions, WebhookSignatureError, type WebhookStatus, applyRetailMarkup, buildCommentPayload, buildJettonTransferPayload, buildPaymentRequest, buildTonConnectMessages, buildTonDeeplink, canonicalUsername, ceilTonTo4dp, ceilUsdToCents, constructEvent, decimalToUnits, defaultShouldRetry, errorFromResponse, expressWebhook, fastifyWebhook, isTerminal, parseRetryAfterMs, parseTonAddress, reconcile, toMicro, toNano, verifyWebhookSignature };
|