@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/dist/index.js ADDED
@@ -0,0 +1,1460 @@
1
+ // src/errors.ts
2
+ var MyStarsError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = new.target.name;
6
+ Object.setPrototypeOf(this, new.target.prototype);
7
+ }
8
+ };
9
+ var MyStarsApiError = class extends MyStarsError {
10
+ /** The envelope `error.code`, or `"unknown"` / `"network"`. */
11
+ code;
12
+ /** HTTP status code (`0` for a network/timeout failure). */
13
+ status;
14
+ /** Buyer-facing Telegram/Fragment message, when the server supplied one. */
15
+ telegramMessage;
16
+ /** The `x-request-id` response header, when present. */
17
+ requestId;
18
+ /** Coarse hint that the failure is potentially transient. The retry policy decides for real. */
19
+ retryable;
20
+ /** The parsed response body (or the thrown error), for debugging. */
21
+ raw;
22
+ /**
23
+ * The `Idempotency-Key` that was sent with the request that failed, when known.
24
+ *
25
+ * `createOrder` stamps this on a thrown error so you can SAFELY retry the
26
+ * create with the SAME key (`{ idempotencyKey: err.idempotencyKey }`) instead
27
+ * of minting a duplicate deliverable when you can't tell whether the order was
28
+ * created server-side. `undefined` for errors not raised by a keyed request.
29
+ */
30
+ idempotencyKey;
31
+ constructor(init) {
32
+ super(init.message);
33
+ this.code = init.code;
34
+ this.status = init.status;
35
+ this.telegramMessage = init.telegramMessage;
36
+ this.requestId = init.requestId;
37
+ this.retryable = init.retryable ?? false;
38
+ this.raw = init.raw;
39
+ this.idempotencyKey = init.idempotencyKey;
40
+ }
41
+ };
42
+ var BadRequestError = class extends MyStarsApiError {
43
+ };
44
+ var UnauthorizedError = class extends MyStarsApiError {
45
+ };
46
+ var ForbiddenError = class extends MyStarsApiError {
47
+ };
48
+ var NotFoundError = class extends MyStarsApiError {
49
+ };
50
+ var ConflictError = class extends MyStarsApiError {
51
+ };
52
+ var IdempotencyConflictError = class extends ConflictError {
53
+ };
54
+ var OrderNotCancellableError = class extends ConflictError {
55
+ };
56
+ var RecipientIneligibleError = class extends MyStarsApiError {
57
+ telegramMessage;
58
+ constructor(init) {
59
+ super(init);
60
+ this.telegramMessage = init.telegramMessage ?? init.message;
61
+ }
62
+ };
63
+ var RateLimitError = class extends MyStarsApiError {
64
+ /** Milliseconds to wait before retrying, from `Retry-After`; `null` for the order-cap/flood limiter. */
65
+ retryAfterMs;
66
+ limit;
67
+ remaining;
68
+ reset;
69
+ /** `"general"` when RFC-9110 `RateLimit-*` headers are present; `"order_cap"` otherwise. */
70
+ kind;
71
+ constructor(init) {
72
+ super(init);
73
+ this.retryAfterMs = init.retryAfterMs ?? null;
74
+ this.limit = init.limit ?? null;
75
+ this.remaining = init.remaining ?? null;
76
+ this.reset = init.reset ?? null;
77
+ this.kind = init.kind;
78
+ }
79
+ };
80
+ var ServiceUnavailableError = class extends MyStarsApiError {
81
+ };
82
+ var InternalServerError = class extends MyStarsApiError {
83
+ };
84
+ var NetworkError = class extends MyStarsApiError {
85
+ };
86
+ var TimeoutError = class extends NetworkError {
87
+ };
88
+ var WebhookSignatureError = class extends MyStarsError {
89
+ };
90
+ var OrderWaitTimeoutError = class extends MyStarsError {
91
+ /** The most recent order snapshot observed before timing out. */
92
+ lastOrder;
93
+ constructor(lastOrder, message) {
94
+ super(message ?? `order ${lastOrder.order_id} did not finish in time (last status: ${lastOrder.status})`);
95
+ this.lastOrder = lastOrder;
96
+ }
97
+ };
98
+ function header(headers, name) {
99
+ const v = headers.get(name);
100
+ return v === null ? void 0 : v;
101
+ }
102
+ function toInt(value) {
103
+ if (value === void 0) return null;
104
+ const n = Number(value);
105
+ return Number.isFinite(n) ? n : null;
106
+ }
107
+ function parseRetryAfterMs(value, now = Date.now()) {
108
+ if (value === void 0) return null;
109
+ const seconds = Number(value);
110
+ if (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1e3));
111
+ const date = Date.parse(value);
112
+ if (Number.isFinite(date)) return Math.max(0, date - now);
113
+ return null;
114
+ }
115
+ function parseEnvelope(status, body) {
116
+ if (body && typeof body === "object" && "error" in body) {
117
+ const err = body.error;
118
+ if (err && typeof err === "object") {
119
+ const e = err;
120
+ return {
121
+ code: typeof e.code === "string" ? e.code : "unknown",
122
+ message: typeof e.message === "string" ? e.message : `HTTP ${status}`,
123
+ telegramMessage: typeof e.telegram_message === "string" ? e.telegram_message : void 0
124
+ };
125
+ }
126
+ if (typeof err === "string") {
127
+ return { code: err, message: err };
128
+ }
129
+ }
130
+ return { code: "unknown", message: `HTTP ${status}` };
131
+ }
132
+ function errorFromResponse(status, body, headers) {
133
+ const { code, message, telegramMessage } = parseEnvelope(status, body);
134
+ const requestId = header(headers, "x-request-id");
135
+ const base = { code, status, message, telegramMessage, requestId, raw: body };
136
+ switch (status) {
137
+ case 400:
138
+ return new BadRequestError(base);
139
+ case 401:
140
+ return new UnauthorizedError(base);
141
+ case 403:
142
+ return new ForbiddenError(base);
143
+ case 404:
144
+ return new NotFoundError(base);
145
+ case 409: {
146
+ const m = message.toLowerCase();
147
+ if (m.includes("idempotency")) return new IdempotencyConflictError(base);
148
+ if (m.includes("cancel")) return new OrderNotCancellableError(base);
149
+ return new ConflictError(base);
150
+ }
151
+ case 422:
152
+ return new RecipientIneligibleError(base);
153
+ case 429: {
154
+ const limit = toInt(header(headers, "ratelimit-limit"));
155
+ const remaining = toInt(header(headers, "ratelimit-remaining"));
156
+ const reset = toInt(header(headers, "ratelimit-reset"));
157
+ const retryAfterMs = parseRetryAfterMs(header(headers, "retry-after"));
158
+ const kind = limit !== null || retryAfterMs !== null ? "general" : "order_cap";
159
+ return new RateLimitError({
160
+ ...base,
161
+ retryable: kind === "general",
162
+ retryAfterMs,
163
+ limit,
164
+ remaining,
165
+ reset,
166
+ kind
167
+ });
168
+ }
169
+ case 503:
170
+ return new ServiceUnavailableError({ ...base, retryable: true });
171
+ case 500:
172
+ return new InternalServerError({ ...base, retryable: true });
173
+ default:
174
+ return new MyStarsApiError({ ...base, retryable: status >= 500 });
175
+ }
176
+ }
177
+
178
+ // src/internal/sleep.ts
179
+ function defaultSleep(ms, signal) {
180
+ return new Promise((resolve, reject) => {
181
+ if (signal?.aborted) {
182
+ reject(new Error("aborted"));
183
+ return;
184
+ }
185
+ const timer = setTimeout(() => {
186
+ signal?.removeEventListener("abort", onAbort);
187
+ resolve();
188
+ }, ms);
189
+ const onAbort = () => {
190
+ clearTimeout(timer);
191
+ reject(new Error("aborted"));
192
+ };
193
+ signal?.addEventListener("abort", onAbort, { once: true });
194
+ });
195
+ }
196
+
197
+ // src/http/retry.ts
198
+ function defaultShouldRetry(ctx) {
199
+ if (!ctx.idempotent) return false;
200
+ const e = ctx.error;
201
+ if (e instanceof NetworkError) return true;
202
+ if (e instanceof ServiceUnavailableError) return true;
203
+ if (e instanceof InternalServerError) return true;
204
+ if (e instanceof RateLimitError) return e.kind === "general";
205
+ return e.retryable;
206
+ }
207
+ var DEFAULTS = {
208
+ maxRetries: 3,
209
+ baseDelayMs: 500,
210
+ maxDelayMs: 3e4,
211
+ jitter: "full",
212
+ respectRetryAfter: true,
213
+ retryOn: defaultShouldRetry
214
+ };
215
+ function resolveRetryPolicy(policy) {
216
+ if (policy === false) return { ...DEFAULTS, maxRetries: 0 };
217
+ if (!policy) return DEFAULTS;
218
+ return {
219
+ maxRetries: policy.maxRetries ?? DEFAULTS.maxRetries,
220
+ baseDelayMs: policy.baseDelayMs ?? DEFAULTS.baseDelayMs,
221
+ maxDelayMs: policy.maxDelayMs ?? DEFAULTS.maxDelayMs,
222
+ jitter: policy.jitter ?? DEFAULTS.jitter,
223
+ respectRetryAfter: policy.respectRetryAfter ?? DEFAULTS.respectRetryAfter,
224
+ retryOn: policy.retryOn ?? DEFAULTS.retryOn
225
+ };
226
+ }
227
+ function computeDelayMs(ctx, policy, random = Math.random) {
228
+ const exp = policy.baseDelayMs * 2 ** ctx.attempt;
229
+ const capped = Math.min(exp, policy.maxDelayMs);
230
+ let delay = policy.jitter === "full" ? random() * capped : capped;
231
+ if (policy.respectRetryAfter && ctx.error instanceof RateLimitError && ctx.error.retryAfterMs !== null) {
232
+ delay = Math.max(delay, Math.min(ctx.error.retryAfterMs, policy.maxDelayMs));
233
+ }
234
+ return Math.round(delay);
235
+ }
236
+
237
+ // src/http/transport.ts
238
+ var MAX_RESPONSE_BYTES = 4e6;
239
+ function buildUrl(baseUrl, path, query) {
240
+ const base = baseUrl.replace(/\/+$/, "");
241
+ const suffix = path.startsWith("/") ? path : `/${path}`;
242
+ let url = `${base}${suffix}`;
243
+ if (query) {
244
+ const params = new URLSearchParams();
245
+ for (const [k, v] of Object.entries(query)) {
246
+ if (v !== void 0) params.set(k, String(v));
247
+ }
248
+ const qs = params.toString();
249
+ if (qs) url += `?${qs}`;
250
+ }
251
+ return url;
252
+ }
253
+ async function readJson(res) {
254
+ const text = await readBoundedText(res);
255
+ if (text.length === 0) return void 0;
256
+ try {
257
+ return JSON.parse(text);
258
+ } catch {
259
+ throw new MyStarsApiError({
260
+ code: "invalid_response",
261
+ status: res.status,
262
+ message: `Response was not valid JSON (HTTP ${res.status})`,
263
+ // A 5xx with an HTML body (Cloudflare/nginx gateway page) is transient → retryable;
264
+ // a 2xx with malformed JSON is not (retrying won't fix a bad success body).
265
+ retryable: res.status >= 500,
266
+ raw: text.slice(0, 500)
267
+ });
268
+ }
269
+ }
270
+ async function readBoundedText(res) {
271
+ const body = res.body;
272
+ if (!body || typeof body.getReader !== "function") {
273
+ const text = await res.text();
274
+ if (text.length > MAX_RESPONSE_BYTES) {
275
+ throw new MyStarsApiError({
276
+ code: "response_too_large",
277
+ status: res.status,
278
+ message: "Response body exceeded the size limit"
279
+ });
280
+ }
281
+ return text;
282
+ }
283
+ const reader = body.getReader();
284
+ const chunks = [];
285
+ let total = 0;
286
+ for (; ; ) {
287
+ const { done, value } = await reader.read();
288
+ if (done) break;
289
+ if (value) {
290
+ total += value.byteLength;
291
+ if (total > MAX_RESPONSE_BYTES) {
292
+ await reader.cancel();
293
+ throw new MyStarsApiError({
294
+ code: "response_too_large",
295
+ status: res.status,
296
+ message: "Response body exceeded the size limit"
297
+ });
298
+ }
299
+ chunks.push(value);
300
+ }
301
+ }
302
+ return new TextDecoder().decode(concat(chunks, total));
303
+ }
304
+ function concat(chunks, total) {
305
+ const out = new Uint8Array(total);
306
+ let offset = 0;
307
+ for (const c of chunks) {
308
+ out.set(c, offset);
309
+ offset += c.byteLength;
310
+ }
311
+ return out;
312
+ }
313
+ var Transport = class {
314
+ opts;
315
+ sleep;
316
+ random;
317
+ constructor(opts) {
318
+ this.opts = opts;
319
+ this.sleep = opts.sleep ?? defaultSleep;
320
+ this.random = opts.random ?? Math.random;
321
+ }
322
+ async request(params) {
323
+ const url = buildUrl(this.opts.baseUrl, params.path, params.query);
324
+ const idempotent = params.idempotent ?? (params.method === "GET" || params.idempotencyKey !== void 0);
325
+ const policy = this.opts.retry;
326
+ let attempt = 0;
327
+ for (; ; ) {
328
+ if (params.signal?.aborted) {
329
+ throw new NetworkError({ code: "aborted", status: 0, message: "request aborted by caller" });
330
+ }
331
+ try {
332
+ return await this.attempt(url, params);
333
+ } catch (err) {
334
+ const apiError = err instanceof MyStarsApiError ? err : wrapUnknown(err);
335
+ const ctx = { method: params.method, path: params.path, attempt, idempotent, error: apiError };
336
+ const willRetry = attempt < policy.maxRetries && !params.signal?.aborted && policy.retryOn(ctx);
337
+ if (!willRetry) throw apiError;
338
+ const delayMs = computeDelayMs(ctx, policy, this.random);
339
+ if (this.opts.interceptors?.onRetry) {
340
+ await this.opts.interceptors.onRetry({
341
+ method: params.method,
342
+ url,
343
+ attempt: attempt + 1,
344
+ delayMs,
345
+ reason: `${apiError.code} (HTTP ${apiError.status})`
346
+ });
347
+ }
348
+ await this.sleep(delayMs, params.signal);
349
+ attempt += 1;
350
+ }
351
+ }
352
+ }
353
+ async attempt(url, params) {
354
+ const headers = new Headers();
355
+ headers.set("Accept", "application/json");
356
+ headers.set("X-Api-Key", this.opts.apiKey);
357
+ if (this.opts.userAgent) headers.set("User-Agent", this.opts.userAgent);
358
+ if (params.idempotencyKey !== void 0) headers.set("Idempotency-Key", params.idempotencyKey);
359
+ let bodyText;
360
+ if (params.body !== void 0) {
361
+ headers.set("Content-Type", "application/json");
362
+ bodyText = JSON.stringify(params.body);
363
+ }
364
+ if (this.opts.interceptors?.onRequest) {
365
+ await this.opts.interceptors.onRequest({
366
+ method: params.method,
367
+ url,
368
+ idempotencyKey: params.idempotencyKey
369
+ });
370
+ }
371
+ const controller = new AbortController();
372
+ let timedOut = false;
373
+ const timer = setTimeout(() => {
374
+ timedOut = true;
375
+ controller.abort();
376
+ }, this.opts.timeoutMs);
377
+ const onCallerAbort = () => controller.abort();
378
+ params.signal?.addEventListener("abort", onCallerAbort, { once: true });
379
+ const startedAt = Date.now();
380
+ let res;
381
+ let data;
382
+ try {
383
+ res = await this.opts.fetchImpl(url, {
384
+ method: params.method,
385
+ headers,
386
+ body: bodyText,
387
+ signal: controller.signal
388
+ });
389
+ data = await readJson(res);
390
+ } catch (err) {
391
+ if (timedOut) {
392
+ throw new TimeoutError({
393
+ code: "timeout",
394
+ status: 0,
395
+ message: `request timed out after ${this.opts.timeoutMs}ms`,
396
+ retryable: true
397
+ });
398
+ }
399
+ if (params.signal?.aborted) {
400
+ throw new NetworkError({ code: "aborted", status: 0, message: "request aborted by caller" });
401
+ }
402
+ if (err instanceof MyStarsApiError) throw err;
403
+ throw new NetworkError({
404
+ code: "network",
405
+ status: 0,
406
+ message: err instanceof Error ? err.message : "network request failed",
407
+ retryable: true,
408
+ raw: err
409
+ });
410
+ } finally {
411
+ clearTimeout(timer);
412
+ params.signal?.removeEventListener("abort", onCallerAbort);
413
+ }
414
+ const durationMs = Date.now() - startedAt;
415
+ const requestId = res.headers.get("x-request-id") ?? void 0;
416
+ if (this.opts.interceptors?.onResponse) {
417
+ await this.opts.interceptors.onResponse({
418
+ method: params.method,
419
+ url,
420
+ status: res.status,
421
+ durationMs,
422
+ requestId
423
+ });
424
+ }
425
+ if (!res.ok) {
426
+ throw errorFromResponse(res.status, data, res.headers);
427
+ }
428
+ return { status: res.status, data, headers: res.headers };
429
+ }
430
+ };
431
+ function wrapUnknown(err) {
432
+ return new NetworkError({
433
+ code: "network",
434
+ status: 0,
435
+ message: err instanceof Error ? err.message : "request failed",
436
+ retryable: true,
437
+ raw: err
438
+ });
439
+ }
440
+
441
+ // src/internal/uuid.ts
442
+ var HEX = [];
443
+ for (let i = 0; i < 256; i++) HEX.push((i + 256).toString(16).slice(1));
444
+ function uuidv4() {
445
+ const c = globalThis.crypto;
446
+ if (c && typeof c.randomUUID === "function") {
447
+ return c.randomUUID();
448
+ }
449
+ const bytes = new Uint8Array(16);
450
+ if (c && typeof c.getRandomValues === "function") {
451
+ c.getRandomValues(bytes);
452
+ } else {
453
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
454
+ }
455
+ bytes[6] = bytes[6] & 15 | 64;
456
+ bytes[8] = bytes[8] & 63 | 128;
457
+ const hex = HEX;
458
+ const b = bytes;
459
+ return hex[b[0]] + hex[b[1]] + hex[b[2]] + hex[b[3]] + "-" + hex[b[4]] + hex[b[5]] + "-" + hex[b[6]] + hex[b[7]] + "-" + hex[b[8]] + hex[b[9]] + "-" + hex[b[10]] + hex[b[11]] + hex[b[12]] + hex[b[13]] + hex[b[14]] + hex[b[15]];
460
+ }
461
+
462
+ // src/internal/validate.ts
463
+ var STARS_MIN_QUANTITY = 50;
464
+ var STARS_MAX_QUANTITY = 1e6;
465
+ var PREMIUM_MONTHS = [3, 6, 12];
466
+ var USERNAME_RE = /^[a-z0-9_]{1,32}$/;
467
+ var MyStarsValidationError = class extends MyStarsError {
468
+ };
469
+ function canonicalUsername(input) {
470
+ if (typeof input !== "string") {
471
+ throw new MyStarsValidationError("recipient username must be a string");
472
+ }
473
+ const canon = input.trim().replace(/^@+/, "").toLowerCase();
474
+ if (!USERNAME_RE.test(canon)) {
475
+ throw new MyStarsValidationError(
476
+ `invalid recipient username "${input}" \u2014 expected 1-32 chars of [a-z0-9_] (a leading @ is allowed)`
477
+ );
478
+ }
479
+ return canon;
480
+ }
481
+ function assertStarsQuantity(quantity) {
482
+ if (!Number.isInteger(quantity) || quantity < STARS_MIN_QUANTITY || quantity > STARS_MAX_QUANTITY) {
483
+ throw new MyStarsValidationError(
484
+ `stars quantity must be an integer in [${STARS_MIN_QUANTITY}, ${STARS_MAX_QUANTITY}], got ${quantity}`
485
+ );
486
+ }
487
+ }
488
+ function assertPremiumMonths(months) {
489
+ if (!PREMIUM_MONTHS.includes(months)) {
490
+ throw new MyStarsValidationError(
491
+ `premium months must be one of ${PREMIUM_MONTHS.join(", ")}, got ${months}`
492
+ );
493
+ }
494
+ }
495
+ function assertOrderType(type) {
496
+ if (type !== "stars" && type !== "premium") {
497
+ throw new MyStarsValidationError(`type must be "stars" or "premium", got ${String(type)}`);
498
+ }
499
+ }
500
+
501
+ // src/tracking/pager.ts
502
+ var OrdersPager = class {
503
+ fetchPage;
504
+ startCursor;
505
+ /**
506
+ * @param fetchPage - fetches a page given a cursor (the client wires this to the transport)
507
+ * @param startCursor - an optional cursor to begin from (resumes mid-stream)
508
+ */
509
+ constructor(fetchPage, startCursor) {
510
+ this.fetchPage = fetchPage;
511
+ this.startCursor = startCursor;
512
+ }
513
+ /** Fetch a single page. Pass the previous page's `next_cursor` to advance. */
514
+ page(cursor) {
515
+ return this.fetchPage(cursor);
516
+ }
517
+ /** Yield one page at a time until `next_cursor` is null. */
518
+ async *pages() {
519
+ let cursor = this.startCursor;
520
+ for (; ; ) {
521
+ const page = await this.fetchPage(cursor);
522
+ yield page;
523
+ if (!page.next_cursor) break;
524
+ cursor = page.next_cursor;
525
+ }
526
+ }
527
+ /** Yield every order across all pages. */
528
+ async *[Symbol.asyncIterator]() {
529
+ for await (const page of this.pages()) {
530
+ for (const order of page.orders) yield order;
531
+ }
532
+ }
533
+ /** Collect every order across all pages into a single array. */
534
+ async all() {
535
+ const out = [];
536
+ for await (const order of this) out.push(order);
537
+ return out;
538
+ }
539
+ };
540
+
541
+ // src/types.ts
542
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
543
+ "delivered",
544
+ "failed",
545
+ "reversed",
546
+ "expired",
547
+ "cancelled"
548
+ ]);
549
+ function isTerminal(status) {
550
+ return TERMINAL_STATUSES.has(status);
551
+ }
552
+ var WEBHOOK_TERMINAL = /* @__PURE__ */ new Set([
553
+ "delivered",
554
+ "failed",
555
+ "reversed",
556
+ "expired"
557
+ ]);
558
+ var INITIAL_ORDER_STATUS = "awaiting_payment";
559
+ var CANCELLABLE_STATUSES = /* @__PURE__ */ new Set(["awaiting_payment"]);
560
+
561
+ // src/tracking/reconcile.ts
562
+ async function reconcile(client, opts) {
563
+ const sinceMs = opts.since !== void 0 ? new Date(opts.since).getTime() : void 0;
564
+ const missed = [];
565
+ for await (const order of client.listOrders({ status: opts.status, limit: opts.limit })) {
566
+ if (sinceMs !== void 0 && new Date(order.created_at).getTime() < sinceMs) break;
567
+ if (!isTerminal(order.status)) continue;
568
+ if (!await opts.isKnown(order)) {
569
+ missed.push(order);
570
+ await opts.onMissed?.(order);
571
+ }
572
+ }
573
+ return missed;
574
+ }
575
+
576
+ // src/tracking/waitForOrder.ts
577
+ async function waitForOrder(orderId, options, deps) {
578
+ const pollIntervalMs = options.pollIntervalMs ?? 2e3;
579
+ const maxPollIntervalMs = options.maxPollIntervalMs ?? 15e3;
580
+ const backoffFactor = options.backoffFactor ?? 1.5;
581
+ const maxWaitMs = options.maxWaitMs ?? 30 * 60 * 1e3;
582
+ const jitter = options.jitter ?? "full";
583
+ const until = options.until ?? ((o) => isTerminal(o.status));
584
+ const deadline = deps.now() + maxWaitMs;
585
+ let interval = pollIntervalMs;
586
+ let lastStatus;
587
+ let lastOrder;
588
+ for (; ; ) {
589
+ lastOrder = await deps.getOrder(orderId, { signal: options.signal });
590
+ if (lastOrder.status !== lastStatus) {
591
+ lastStatus = lastOrder.status;
592
+ options.onUpdate?.(lastOrder);
593
+ }
594
+ if (until(lastOrder)) return lastOrder;
595
+ const remaining = deadline - deps.now();
596
+ if (remaining <= 0) throw new OrderWaitTimeoutError(lastOrder);
597
+ const base = Math.min(interval, maxPollIntervalMs);
598
+ const jittered = jitter === "full" ? base * (0.5 + 0.5 * deps.random()) : base;
599
+ await deps.sleep(Math.max(0, Math.min(jittered, remaining)), options.signal);
600
+ interval = Math.min(interval * backoffFactor, maxPollIntervalMs);
601
+ }
602
+ }
603
+
604
+ // src/version.ts
605
+ var CONTRACT_VERSION = "1.9.0";
606
+ var SDK_VERSION = "0.1.2";
607
+
608
+ // src/client.ts
609
+ var PRODUCTION_BASE_URL = "https://api.mystars.tg/v1";
610
+ function resolveBaseUrl(opts) {
611
+ return opts.baseUrl ?? PRODUCTION_BASE_URL;
612
+ }
613
+ var MyStarsClient = class _MyStarsClient {
614
+ transport;
615
+ idempotencyKeyFactory;
616
+ now;
617
+ sleep;
618
+ random;
619
+ /**
620
+ * @param opts - the {@link MyStarsClientOptions} (at minimum `apiKey`). Prefer the
621
+ * {@link MyStarsClient.production} factory.
622
+ * @throws `Error` if `apiKey` is missing, or no `fetch` is available (Node <18 — pass `fetch` explicitly)
623
+ */
624
+ constructor(opts) {
625
+ if (!opts.apiKey) throw new Error("MyStarsClient: `apiKey` is required");
626
+ const fetchImpl = opts.fetch ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
627
+ if (!fetchImpl) {
628
+ throw new Error("MyStarsClient: no global fetch found \u2014 pass `fetch` explicitly (Node <18 or a custom runtime)");
629
+ }
630
+ this.idempotencyKeyFactory = opts.idempotencyKeyFactory ?? uuidv4;
631
+ this.now = opts.now ?? (() => Date.now());
632
+ this.random = opts.random ?? Math.random;
633
+ this.sleep = opts.sleep ?? defaultSleep;
634
+ this.transport = new Transport({
635
+ apiKey: opts.apiKey,
636
+ baseUrl: resolveBaseUrl(opts),
637
+ fetchImpl,
638
+ timeoutMs: opts.timeoutMs ?? 3e4,
639
+ retry: resolveRetryPolicy(opts.retry),
640
+ userAgent: opts.userAgent ?? `mystars-faas-sdk/${SDK_VERSION}`,
641
+ interceptors: opts.interceptors,
642
+ sleep: opts.sleep,
643
+ random: opts.random
644
+ });
645
+ }
646
+ /**
647
+ * Build a client pointed at production (`api.mystars.tg`).
648
+ *
649
+ * @param apiKey - your tenant `faas_…` API key
650
+ * @param opts - any other {@link MyStarsClientOptions} except `apiKey`/`baseUrl`
651
+ * @returns a configured client
652
+ * @example
653
+ * ```ts
654
+ * const client = MyStarsClient.production(process.env.MYSTARS_API_KEY!);
655
+ * ```
656
+ */
657
+ static production(apiKey, opts) {
658
+ return new _MyStarsClient({ ...opts, apiKey });
659
+ }
660
+ /**
661
+ * `GET /v1/currencies` — the supported on-chain payment currencies.
662
+ *
663
+ * @param opts - optional `signal` to abort the request
664
+ * @returns the supported {@link CurrencyInfo} list
665
+ * @throws `MyStarsApiError` on an API/network failure
666
+ */
667
+ async listCurrencies(opts = {}) {
668
+ const { data } = await this.transport.request({
669
+ method: "GET",
670
+ path: "/currencies",
671
+ signal: opts.signal
672
+ });
673
+ return data.currencies;
674
+ }
675
+ /**
676
+ * `GET /v1/products` — the orderable product catalog (price-free).
677
+ *
678
+ * @param opts - optional `signal` to abort the request
679
+ * @returns the {@link Product} catalog (buyable shapes + limits)
680
+ * @throws `MyStarsApiError` on an API/network failure
681
+ */
682
+ async listProducts(opts = {}) {
683
+ const { data } = await this.transport.request({
684
+ method: "GET",
685
+ path: "/products",
686
+ signal: opts.signal
687
+ });
688
+ return data.products;
689
+ }
690
+ /**
691
+ * `GET /v1/pricing` — quote the all-in price for an item. Probe-rate-limited (30/min).
692
+ *
693
+ * @param params - `{ type, quantity | months, payment_currency? }`; `payment_currency` defaults server-side
694
+ * @param opts - optional `signal` to abort the request
695
+ * @returns the {@link PricingQuote} — `amount` is the all-in total in `currency`; `fee` is itemized for `usdt_ton`
696
+ * @throws `MyStarsValidationError` if `quantity`/`months` is out of range
697
+ * @throws `RateLimitError` (429) if the probe limit is exceeded
698
+ * @throws `ServiceUnavailableError` (503) if a price source is temporarily down
699
+ * @example
700
+ * ```ts
701
+ * const q = await client.getPricing({ type: "stars", quantity: 500, payment_currency: "ton" });
702
+ * console.log(`pay ${q.amount} ${q.currency}`);
703
+ * ```
704
+ */
705
+ async getPricing(params, opts = {}) {
706
+ assertOrderType(params.type);
707
+ const query = {
708
+ type: params.type,
709
+ payment_currency: params.payment_currency
710
+ };
711
+ if (params.type === "stars") {
712
+ assertStarsQuantity(params.quantity);
713
+ query.quantity = params.quantity;
714
+ } else {
715
+ assertPremiumMonths(params.months);
716
+ query.months = params.months;
717
+ }
718
+ const { data } = await this.transport.request({
719
+ method: "GET",
720
+ path: "/pricing",
721
+ query,
722
+ signal: opts.signal
723
+ });
724
+ return data;
725
+ }
726
+ /**
727
+ * `POST /v1/recipients/check` — resolve a `@username` and check eligibility before ordering.
728
+ *
729
+ * @param params - `{ type, recipient: { username }, months? }`; the username is canonicalized (strip `@`, lowercase)
730
+ * @param opts - optional `signal` to abort the request
731
+ * @returns the {@link RecipientCheck} — `resolved`/`eligible`, the display `recipient_name`, and a `reason` when ineligible
732
+ * @throws `MyStarsValidationError` if the username is malformed
733
+ * @throws `MyStarsApiError` on an API/network failure
734
+ */
735
+ async checkRecipient(params, opts = {}) {
736
+ assertOrderType(params.type);
737
+ const body = {
738
+ type: params.type,
739
+ recipient: { username: canonicalUsername(params.recipient.username) }
740
+ };
741
+ if (params.type === "premium" && params.months !== void 0) {
742
+ assertPremiumMonths(params.months);
743
+ body.months = params.months;
744
+ }
745
+ const { data } = await this.transport.request({
746
+ method: "POST",
747
+ path: "/recipients/check",
748
+ body,
749
+ // Read-only and fail-open server-side, so safe to retry on transient errors.
750
+ idempotent: true,
751
+ signal: opts.signal
752
+ });
753
+ return data;
754
+ }
755
+ /**
756
+ * `POST /v1/orders` — create an order. An `Idempotency-Key` is sent once and
757
+ * reused across this call's retries, so a retried create returns the idempotent
758
+ * replay (`replayed: true`) instead of a duplicate.
759
+ *
760
+ * MONEY SAFETY — supply a STABLE key derived from YOUR OWN order id
761
+ * (`{ idempotencyKey: \`order-\${myOrderId}\` }`). The auto-generated uuid only
762
+ * dedupes WITHIN a single `createOrder` call's internal retries; a brand-new
763
+ * call (e.g. your process crashed and re-ran) mints a fresh uuid and can create
764
+ * a SECOND order for the same intent. A caller-stable key makes the create
765
+ * idempotent across process restarts and at-least-once job runners.
766
+ *
767
+ * On failure the thrown {@link MyStarsApiError} carries the `idempotencyKey`
768
+ * that was used — when you can't tell whether the order was created
769
+ * server-side, retry with that exact key (`{ idempotencyKey: err.idempotencyKey }`)
770
+ * to get the idempotent replay rather than a duplicate.
771
+ *
772
+ * @param params - `{ type, recipient: { username }, quantity | months, payment_currency?, callback_url? }`
773
+ * @param opts - optional caller-stable `idempotencyKey` (recommended) and `signal`
774
+ * @returns the {@link CreateOrderResult} — `payment` instruction + `expires_at`; `replayed` is `true` on a 200 idempotent replay
775
+ * @throws `MyStarsValidationError` if inputs are invalid
776
+ * @throws `RecipientIneligibleError` (422) if the recipient cannot receive the item (no order created)
777
+ * @throws `IdempotencyConflictError` (409) if the same key is reused with a different body
778
+ * @throws `MyStarsApiError` on any other API failure (carries the `idempotencyKey` for safe retry)
779
+ * @example
780
+ * ```ts
781
+ * const order = await client.createOrder(
782
+ * { type: "stars", recipient: { username: "durov" }, quantity: 100 },
783
+ * { idempotencyKey: `order-${myOrderId}` },
784
+ * );
785
+ * // pay order.payment.amount → order.payment.pay_to_address, comment = order.payment.memo
786
+ * ```
787
+ */
788
+ async createOrder(params, opts = {}) {
789
+ assertOrderType(params.type);
790
+ const body = {
791
+ type: params.type,
792
+ recipient: { username: canonicalUsername(params.recipient.username) }
793
+ };
794
+ if (params.type === "stars") {
795
+ assertStarsQuantity(params.quantity);
796
+ body.quantity = params.quantity;
797
+ } else {
798
+ assertPremiumMonths(params.months);
799
+ body.months = params.months;
800
+ }
801
+ if (params.payment_currency !== void 0) body.payment_currency = params.payment_currency;
802
+ if (params.callback_url !== void 0) body.callback_url = params.callback_url;
803
+ const idempotencyKey = opts.idempotencyKey ?? this.idempotencyKeyFactory();
804
+ try {
805
+ const { data, status } = await this.transport.request({
806
+ method: "POST",
807
+ path: "/orders",
808
+ body,
809
+ idempotencyKey,
810
+ signal: opts.signal
811
+ });
812
+ return { ...data, replayed: status === 200 };
813
+ } catch (err) {
814
+ if (err instanceof MyStarsApiError && err.idempotencyKey === void 0) {
815
+ err.idempotencyKey = idempotencyKey;
816
+ }
817
+ throw err;
818
+ }
819
+ }
820
+ /**
821
+ * `GET /v1/orders/:id` — fetch one order (tenant-scoped). Polling an
822
+ * `awaiting_payment` order also nudges the server's on-demand payment detection.
823
+ *
824
+ * @param orderId - the order UUID
825
+ * @param opts - optional `signal` to abort the request
826
+ * @returns the {@link Order}
827
+ * @throws `NotFoundError` (404) if no such order exists for this tenant
828
+ * @throws `MyStarsApiError` on any other API failure
829
+ */
830
+ async getOrder(orderId, opts = {}) {
831
+ const { data } = await this.transport.request({
832
+ method: "GET",
833
+ path: `/orders/${encodeURIComponent(orderId)}`,
834
+ signal: opts.signal
835
+ });
836
+ return data;
837
+ }
838
+ /**
839
+ * `POST /v1/orders/:id/cancel` — cancel an `awaiting_payment` order.
840
+ *
841
+ * @param orderId - the order UUID
842
+ * @param opts - optional `signal` to abort the request
843
+ * @returns `{ order_id, status: "cancelled" }`
844
+ * @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`)
845
+ * @throws `MyStarsApiError` on any other API failure
846
+ */
847
+ async cancelOrder(orderId, opts = {}) {
848
+ const { data } = await this.transport.request({
849
+ method: "POST",
850
+ path: `/orders/${encodeURIComponent(orderId)}/cancel`,
851
+ // Safe to retry: the cancel is a guarded transition (no double-action). Note a
852
+ // retry after a lost success can surface OrderNotCancellableError (409) even
853
+ // though the cancel committed — re-check with getOrder() if you catch one.
854
+ idempotent: true,
855
+ signal: opts.signal
856
+ });
857
+ return data;
858
+ }
859
+ listOrdersPage(params, cursor, signal) {
860
+ return this.transport.request({
861
+ method: "GET",
862
+ path: "/orders",
863
+ query: { status: params.status, limit: params.limit, cursor },
864
+ signal
865
+ }).then((r) => r.data);
866
+ }
867
+ /**
868
+ * `GET /v1/orders` — an auto-paginating view: `for await (const order of client.listOrders())`.
869
+ * The pager owns the cursor; `params.cursor` (if given) is the starting page.
870
+ *
871
+ * @param params - optional `status` filter, `limit` (1-100), and a starting `cursor`
872
+ * @param opts - optional `signal` to abort the underlying page fetches
873
+ * @returns an {@link OrdersPager} (async-iterable; `.pages()` / `.page()` / `.all()`)
874
+ * @example
875
+ * ```ts
876
+ * for await (const order of client.listOrders({ status: "delivered" })) {
877
+ * console.log(order.order_id);
878
+ * }
879
+ * ```
880
+ */
881
+ listOrders(params = {}, opts = {}) {
882
+ return new OrdersPager((cursor) => this.listOrdersPage(params, cursor, opts.signal), params.cursor);
883
+ }
884
+ /**
885
+ * Poll `GET /v1/orders/:id` until the order is terminal (or the deadline).
886
+ *
887
+ * @param orderId - the order UUID
888
+ * @param options - the {@link WaitForOrderOptions} (intervals, deadline, `until`, `onUpdate`, `signal`)
889
+ * @returns the final {@link Order} once terminal
890
+ * @throws `OrderWaitTimeoutError` if `maxWaitMs` elapses first (carries the last snapshot)
891
+ * @throws `MyStarsApiError` if a poll fails non-transiently
892
+ * @example
893
+ * ```ts
894
+ * const final = await client.waitForOrder(order.order_id, { onUpdate: (o) => console.log(o.status) });
895
+ * ```
896
+ */
897
+ waitForOrder(orderId, options = {}) {
898
+ return waitForOrder(orderId, options, {
899
+ getOrder: (id, o) => this.getOrder(id, o),
900
+ sleep: this.sleep,
901
+ now: this.now,
902
+ random: this.random
903
+ });
904
+ }
905
+ /**
906
+ * Diff the server's orders against your local store to catch webhook-missed terminal transitions.
907
+ *
908
+ * @param options - the {@link ReconcileOptions}; `isKnown` is required, `since` bounds the scan
909
+ * @returns the missed terminal {@link Order}s, newest-first
910
+ * @throws `MyStarsApiError` if a page fetch fails
911
+ */
912
+ reconcile(options) {
913
+ return reconcile(this, options);
914
+ }
915
+ };
916
+
917
+ // src/webhook/verify.ts
918
+ async function getSubtle() {
919
+ const c = globalThis.crypto;
920
+ if (c?.subtle) return c.subtle;
921
+ try {
922
+ const nodeCrypto = await import('crypto');
923
+ if (nodeCrypto.webcrypto?.subtle) return nodeCrypto.webcrypto.subtle;
924
+ } catch {
925
+ }
926
+ throw new Error("Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime");
927
+ }
928
+ async function hmacSha256Hex(secret, body) {
929
+ const subtle = await getSubtle();
930
+ const enc = new TextEncoder();
931
+ const data = typeof body === "string" ? enc.encode(body) : new Uint8Array(body);
932
+ const key = await subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
933
+ const sig = await subtle.sign("HMAC", key, data);
934
+ const out = new Uint8Array(sig);
935
+ let hex = "";
936
+ for (let i = 0; i < out.length; i++) hex += out[i].toString(16).padStart(2, "0");
937
+ return hex;
938
+ }
939
+ function timingSafeHexEqual(a, b) {
940
+ if (a.length !== b.length) return false;
941
+ let diff = 0;
942
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
943
+ return diff === 0;
944
+ }
945
+ async function verifyWebhookSignature(rawBody, signatureHeader, secret) {
946
+ if (!signatureHeader) return false;
947
+ const expected = await hmacSha256Hex(secret, rawBody);
948
+ let matched = false;
949
+ for (const part of signatureHeader.split(",")) {
950
+ if (timingSafeHexEqual(expected, part.trim())) matched = true;
951
+ }
952
+ return matched;
953
+ }
954
+ async function constructEvent(rawBody, signatureHeader, secret) {
955
+ const ok = await verifyWebhookSignature(rawBody, signatureHeader, secret);
956
+ if (!ok) throw new WebhookSignatureError("webhook signature verification failed");
957
+ const text = typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody);
958
+ let parsed;
959
+ try {
960
+ parsed = JSON.parse(text);
961
+ } catch {
962
+ throw new WebhookSignatureError("webhook body is not valid JSON");
963
+ }
964
+ if (!parsed || typeof parsed !== "object") {
965
+ throw new WebhookSignatureError("webhook body is not a JSON object");
966
+ }
967
+ const obj = parsed;
968
+ if (typeof obj.order_id !== "string") throw new WebhookSignatureError("webhook body is missing order_id");
969
+ if (typeof obj.status !== "string") throw new WebhookSignatureError("webhook body is missing status");
970
+ return parsed;
971
+ }
972
+
973
+ // src/webhook/middleware.ts
974
+ function headerValue(value) {
975
+ if (Array.isArray(value)) return value[0];
976
+ return value;
977
+ }
978
+ function rawBodyString(body) {
979
+ if (typeof body === "string") return body;
980
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
981
+ return void 0;
982
+ }
983
+ function expressWebhook(opts) {
984
+ return async (req, res) => {
985
+ const raw = rawBodyString(req.rawBody) ?? rawBodyString(req.body);
986
+ if (raw === void 0) {
987
+ res.status(400).send("raw body required (mount express.raw on this route)");
988
+ return;
989
+ }
990
+ const sig = headerValue(req.headers["x-faas-signature"]);
991
+ try {
992
+ const secret = typeof opts.secret === "function" ? await opts.secret(req) : opts.secret;
993
+ const event = await constructEvent(raw, sig, secret);
994
+ await opts.onEvent(event, { rawBody: raw, req });
995
+ res.status(200).send("ok");
996
+ } catch (err) {
997
+ const status = err instanceof WebhookSignatureError ? 400 : 500;
998
+ res.status(status).send(status === 400 ? "invalid signature" : "handler error");
999
+ opts.onError?.(err, req);
1000
+ }
1001
+ };
1002
+ }
1003
+ function fastifyWebhook(opts) {
1004
+ return async (req, reply) => {
1005
+ const raw = rawBodyString(req.rawBody) ?? rawBodyString(req.body);
1006
+ if (raw === void 0) {
1007
+ reply.code(400).send("raw body required (use a buffer content-type parser)");
1008
+ return;
1009
+ }
1010
+ const sig = headerValue(req.headers["x-faas-signature"]);
1011
+ try {
1012
+ const secret = typeof opts.secret === "function" ? await opts.secret(req) : opts.secret;
1013
+ const event = await constructEvent(raw, sig, secret);
1014
+ await opts.onEvent(event, { rawBody: raw, req });
1015
+ reply.code(200).send("ok");
1016
+ } catch (err) {
1017
+ const status = err instanceof WebhookSignatureError ? 400 : 500;
1018
+ reply.code(status).send(status === 400 ? "invalid signature" : "handler error");
1019
+ opts.onError?.(err, req);
1020
+ }
1021
+ };
1022
+ }
1023
+
1024
+ // src/pricing/markup.ts
1025
+ function ceilUsdToCents(usd) {
1026
+ const micro = Math.round(usd * 1e6);
1027
+ return Math.ceil(micro / 1e4) / 100;
1028
+ }
1029
+ function ceilTonTo4dp(ton) {
1030
+ const nano = Math.round(ton * 1e9);
1031
+ return Math.ceil(nano / 1e5) / 1e4;
1032
+ }
1033
+ function num(value, label) {
1034
+ const n = Number(value);
1035
+ if (!Number.isFinite(n)) throw new MyStarsValidationError(`${label} must be a finite decimal, got "${value}"`);
1036
+ return n;
1037
+ }
1038
+ function fmtUsd(n) {
1039
+ return n.toFixed(2);
1040
+ }
1041
+ function fmtTon(n) {
1042
+ let s = n.toFixed(4);
1043
+ if (s.includes(".")) s = s.replace(/0+$/, "").replace(/\.$/, "");
1044
+ return s;
1045
+ }
1046
+ function applyRetailMarkup(input, config) {
1047
+ const marginPct = config.marginPct;
1048
+ if (!Number.isFinite(marginPct) || marginPct < 0) {
1049
+ throw new MyStarsValidationError(`marginPct must be a non-negative number, got ${marginPct}`);
1050
+ }
1051
+ const passThrough = config.passThroughProcessingFee ?? true;
1052
+ const amount = num(input.amount, "amount");
1053
+ const factor = 1 + marginPct / 100;
1054
+ if (input.currency === "usdt_ton") {
1055
+ if (!input.fee) {
1056
+ throw new MyStarsValidationError(
1057
+ "usdt_ton amount has no fee breakdown; re-quote via GET /v1/pricing before applying markup"
1058
+ );
1059
+ }
1060
+ const goods = num(input.fee.subtotal, "fee.subtotal");
1061
+ const ourFee = num(input.fee.processing_fee, "fee.processing_fee");
1062
+ const retailGoods2 = ceilUsdToCents(goods * factor);
1063
+ const customerFee = passThrough ? ceilUsdToCents(ourFee) : 0;
1064
+ const markup2 = retailGoods2 - goods;
1065
+ const total = retailGoods2 + customerFee;
1066
+ const profit = total - amount;
1067
+ return {
1068
+ currency: "usdt_ton",
1069
+ wholesaleAmount: input.amount,
1070
+ marginPct,
1071
+ goods: fmtUsd(goods),
1072
+ markup: fmtUsd(markup2),
1073
+ subtotal: fmtUsd(retailGoods2),
1074
+ processingFee: fmtUsd(customerFee),
1075
+ total: fmtUsd(total),
1076
+ profit: fmtUsd(profit),
1077
+ lineItems: [
1078
+ { label: "Goods", amount: fmtUsd(goods) },
1079
+ { label: `Retail margin (${marginPct}%)`, amount: fmtUsd(markup2) },
1080
+ { label: "Processing fee", amount: fmtUsd(customerFee) }
1081
+ ]
1082
+ };
1083
+ }
1084
+ const retailGoods = ceilTonTo4dp(amount * factor);
1085
+ const markup = retailGoods - amount;
1086
+ return {
1087
+ currency: "ton",
1088
+ wholesaleAmount: input.amount,
1089
+ marginPct,
1090
+ goods: fmtTon(amount),
1091
+ markup: fmtTon(markup),
1092
+ subtotal: fmtTon(retailGoods),
1093
+ processingFee: "0",
1094
+ total: fmtTon(retailGoods),
1095
+ profit: fmtTon(markup),
1096
+ lineItems: [
1097
+ { label: "Goods", amount: fmtTon(amount) },
1098
+ { label: `Retail margin (${marginPct}%)`, amount: fmtTon(markup) }
1099
+ ]
1100
+ };
1101
+ }
1102
+
1103
+ // src/payment/util.ts
1104
+ function bytesToBase64(bytes) {
1105
+ let binary = "";
1106
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
1107
+ if (typeof btoa === "function") return btoa(binary);
1108
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
1109
+ throw new Error("no base64 encoder available in this runtime");
1110
+ }
1111
+ function base64ToBytes(b64) {
1112
+ if (typeof atob === "function") {
1113
+ const bin = atob(b64);
1114
+ const out = new Uint8Array(bin.length);
1115
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1116
+ return out;
1117
+ }
1118
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(b64, "base64"));
1119
+ throw new Error("no base64 decoder available in this runtime");
1120
+ }
1121
+ function crc32c(data) {
1122
+ const POLY = 2197175160;
1123
+ let crc = 4294967295;
1124
+ for (let i = 0; i < data.length; i++) {
1125
+ crc ^= data[i];
1126
+ for (let j = 0; j < 8; j++) {
1127
+ crc = crc & 1 ? crc >>> 1 ^ POLY : crc >>> 1;
1128
+ }
1129
+ }
1130
+ return (crc ^ 4294967295) >>> 0;
1131
+ }
1132
+ function crc16xmodem(data) {
1133
+ let crc = 0;
1134
+ for (let i = 0; i < data.length; i++) {
1135
+ crc ^= data[i] << 8;
1136
+ for (let j = 0; j < 8; j++) {
1137
+ crc = crc & 32768 ? crc << 1 ^ 4129 : crc << 1;
1138
+ crc &= 65535;
1139
+ }
1140
+ }
1141
+ return crc;
1142
+ }
1143
+
1144
+ // src/payment/cell.ts
1145
+ var MAX_COMMENT_BYTES = 123;
1146
+ function buildCommentPayload(comment) {
1147
+ const textBytes = new TextEncoder().encode(comment);
1148
+ if (32 + textBytes.length * 8 > 1023) {
1149
+ throw new MyStarsValidationError(
1150
+ `comment is ${textBytes.length} UTF-8 bytes; max ${MAX_COMMENT_BYTES} fit in a single-cell op-0 payload`
1151
+ );
1152
+ }
1153
+ const totalBits = 32 + textBytes.length * 8;
1154
+ const dataBytes = Math.ceil(totalBits / 8);
1155
+ const data = new Uint8Array(dataBytes);
1156
+ data.set(textBytes, 4);
1157
+ const d1 = 0;
1158
+ const d2Actual = totalBits % 8 === 0 ? totalBits / 8 * 2 : Math.floor(totalBits / 8) * 2 + 1;
1159
+ const cellData = new Uint8Array(Math.ceil(totalBits / 8));
1160
+ cellData.set(data.subarray(0, cellData.length));
1161
+ if (totalBits % 8 !== 0) {
1162
+ const shift = 8 - totalBits % 8;
1163
+ cellData[cellData.length - 1] |= 1 << shift - 1;
1164
+ }
1165
+ const cellPayload = new Uint8Array(2 + cellData.length);
1166
+ cellPayload[0] = d1;
1167
+ cellPayload[1] = d2Actual;
1168
+ cellPayload.set(cellData, 2);
1169
+ const cellsSize = cellPayload.length;
1170
+ const offsetBytes = 1;
1171
+ const headerSize = 4 + 1 + 1 + 1 + 1 + 1 + offsetBytes + 1;
1172
+ const boc = new Uint8Array(headerSize + cellsSize + 4);
1173
+ let pos = 0;
1174
+ boc[pos++] = 181;
1175
+ boc[pos++] = 238;
1176
+ boc[pos++] = 156;
1177
+ boc[pos++] = 114;
1178
+ boc[pos++] = 1 << 6 | offsetBytes;
1179
+ boc[pos++] = offsetBytes;
1180
+ boc[pos++] = 1;
1181
+ boc[pos++] = 1;
1182
+ boc[pos++] = 0;
1183
+ boc[pos++] = cellsSize;
1184
+ boc[pos++] = 0;
1185
+ boc.set(cellPayload, pos);
1186
+ pos += cellsSize;
1187
+ const crc = crc32c(boc.subarray(0, pos));
1188
+ boc[pos++] = crc & 255;
1189
+ boc[pos++] = crc >> 8 & 255;
1190
+ boc[pos++] = crc >> 16 & 255;
1191
+ boc[pos++] = crc >> 24 & 255;
1192
+ return bytesToBase64(boc);
1193
+ }
1194
+
1195
+ // src/payment/jetton.ts
1196
+ var MAX_MEMO_BYTES = 123;
1197
+ var FORWARD_TON_AMOUNT_NANO = BigInt(0);
1198
+ var JETTON_TRANSFER_OP = 260734629;
1199
+ var BitBuilder = class {
1200
+ fullBytes = [];
1201
+ currentByte = 0;
1202
+ bitPos = 0;
1203
+ storeUint(value, bits) {
1204
+ const v = BigInt(value);
1205
+ for (let i = bits - 1; i >= 0; i--) {
1206
+ const bit = Number(v >> BigInt(i) & BigInt(1));
1207
+ this.currentByte = this.currentByte << 1 | bit;
1208
+ this.bitPos++;
1209
+ if (this.bitPos === 8) {
1210
+ this.fullBytes.push(this.currentByte);
1211
+ this.currentByte = 0;
1212
+ this.bitPos = 0;
1213
+ }
1214
+ }
1215
+ return this;
1216
+ }
1217
+ storeBit(bit) {
1218
+ return this.storeUint(bit, 1);
1219
+ }
1220
+ /** Store a Coins value (VarUInteger 16): 4-bit byte-length prefix + big-endian bytes. Zero → 4 zero bits. */
1221
+ storeCoins(amount) {
1222
+ if (amount === BigInt(0)) return this.storeUint(0, 4);
1223
+ const hex = amount.toString(16);
1224
+ const byteLen = Math.ceil(hex.length / 2);
1225
+ this.storeUint(byteLen, 4);
1226
+ const padded = hex.padStart(byteLen * 2, "0");
1227
+ for (let i = 0; i < byteLen; i++) {
1228
+ this.storeUint(parseInt(padded.slice(i * 2, i * 2 + 2), 16), 8);
1229
+ }
1230
+ return this;
1231
+ }
1232
+ /** Store MsgAddress std (addr_std$10). Accepts friendly (base64url) or raw (`wc:hex`) forms. */
1233
+ storeAddress(address) {
1234
+ const { workchain, hash } = parseTonAddress(address);
1235
+ this.storeUint(2, 2);
1236
+ this.storeBit(0);
1237
+ this.storeUint(workchain < 0 ? 256 + workchain : workchain, 8);
1238
+ for (const byte of hash) this.storeUint(byte, 8);
1239
+ return this;
1240
+ }
1241
+ /** Store a UTF-8 string as raw bytes (no length prefix). */
1242
+ storeStringTail(text) {
1243
+ const bytes = new TextEncoder().encode(text);
1244
+ for (const byte of bytes) this.storeUint(byte, 8);
1245
+ return this;
1246
+ }
1247
+ get totalBits() {
1248
+ return this.fullBytes.length * 8 + this.bitPos;
1249
+ }
1250
+ finalize() {
1251
+ const bits = this.totalBits;
1252
+ const byteLen = Math.ceil(bits / 8);
1253
+ const result = new Uint8Array(byteLen);
1254
+ for (let i = 0; i < this.fullBytes.length; i++) result[i] = this.fullBytes[i];
1255
+ if (this.bitPos > 0) result[this.fullBytes.length] = this.currentByte << 8 - this.bitPos;
1256
+ return { data: result, bits };
1257
+ }
1258
+ };
1259
+ function parseTonAddress(address) {
1260
+ if (address.includes(":")) {
1261
+ const colonIdx = address.indexOf(":");
1262
+ const wc = address.slice(0, colonIdx);
1263
+ const hashHex = address.slice(colonIdx + 1);
1264
+ if (!/^-?\d+$/.test(wc)) throw new Error(`Invalid TON address: bad workchain "${wc}"`);
1265
+ const workchain2 = Number(wc);
1266
+ if (workchain2 !== 0 && workchain2 !== -1) {
1267
+ throw new Error(`Invalid TON address: workchain must be 0 or -1, got ${workchain2}`);
1268
+ }
1269
+ if (!hashHex || hashHex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(hashHex)) {
1270
+ throw new Error("Invalid TON address: hash must be 64 hex characters");
1271
+ }
1272
+ const hash2 = new Uint8Array(32);
1273
+ for (let i = 0; i < 32; i++) hash2[i] = parseInt(hashHex.slice(i * 2, i * 2 + 2), 16);
1274
+ return { workchain: workchain2, hash: hash2 };
1275
+ }
1276
+ const standard = address.replace(/-/g, "+").replace(/_/g, "/");
1277
+ let bytes;
1278
+ try {
1279
+ bytes = base64ToBytes(standard);
1280
+ } catch {
1281
+ throw new Error("Invalid TON address: not valid base64");
1282
+ }
1283
+ if (bytes.length !== 36) throw new Error(`Invalid TON address: expected 36 bytes, got ${bytes.length}`);
1284
+ const payload = bytes.subarray(0, 34);
1285
+ const expectedCrc = bytes[34] << 8 | bytes[35];
1286
+ const actualCrc = crc16xmodem(payload);
1287
+ if (expectedCrc !== actualCrc) {
1288
+ throw new Error(
1289
+ `Invalid TON address: checksum mismatch (expected ${expectedCrc.toString(16)}, got ${actualCrc.toString(16)})`
1290
+ );
1291
+ }
1292
+ const workchain = bytes[1] > 127 ? bytes[1] - 256 : bytes[1];
1293
+ const hash = bytes.slice(2, 34);
1294
+ return { workchain, hash };
1295
+ }
1296
+ function serializeCell(data, bits, refIndices) {
1297
+ const dataLen = Math.ceil(bits / 8);
1298
+ const d1 = refIndices.length;
1299
+ const d2 = bits % 8 === 0 ? bits / 8 * 2 : Math.floor(bits / 8) * 2 + 1;
1300
+ const cellData = new Uint8Array(dataLen);
1301
+ cellData.set(data.subarray(0, dataLen));
1302
+ if (bits % 8 !== 0) {
1303
+ const remaining = bits % 8;
1304
+ cellData[dataLen - 1] |= 1 << 8 - remaining - 1;
1305
+ }
1306
+ const result = new Uint8Array(2 + dataLen + refIndices.length);
1307
+ result[0] = d1;
1308
+ result[1] = d2;
1309
+ result.set(cellData, 2);
1310
+ for (let i = 0; i < refIndices.length; i++) result[2 + dataLen + i] = refIndices[i];
1311
+ return result;
1312
+ }
1313
+ function serializeBoC(cells) {
1314
+ const cellCount = cells.length;
1315
+ const totalCellsSize = cells.reduce((sum, c) => sum + c.length, 0);
1316
+ const offsetBytes = totalCellsSize < 256 ? 1 : 2;
1317
+ const refByteSize = 1;
1318
+ const headerSize = 4 + 1 + 1 + refByteSize * 3 + offsetBytes + refByteSize;
1319
+ const boc = new Uint8Array(headerSize + totalCellsSize + 4);
1320
+ let pos = 0;
1321
+ boc[pos++] = 181;
1322
+ boc[pos++] = 238;
1323
+ boc[pos++] = 156;
1324
+ boc[pos++] = 114;
1325
+ boc[pos++] = 1 << 6 | refByteSize;
1326
+ boc[pos++] = offsetBytes;
1327
+ boc[pos++] = cellCount;
1328
+ boc[pos++] = 1;
1329
+ boc[pos++] = 0;
1330
+ if (offsetBytes === 2) boc[pos++] = totalCellsSize >> 8 & 255;
1331
+ boc[pos++] = totalCellsSize & 255;
1332
+ boc[pos++] = 0;
1333
+ for (const cell of cells) {
1334
+ boc.set(cell, pos);
1335
+ pos += cell.length;
1336
+ }
1337
+ const crc = crc32c(boc.subarray(0, pos));
1338
+ boc[pos++] = crc & 255;
1339
+ boc[pos++] = crc >> 8 & 255;
1340
+ boc[pos++] = crc >> 16 & 255;
1341
+ boc[pos++] = crc >> 24 & 255;
1342
+ return bytesToBase64(boc);
1343
+ }
1344
+ function buildJettonTransferPayload(amountMicro, destination, sender, memo) {
1345
+ const memoBytes = new TextEncoder().encode(memo).length;
1346
+ if (32 + memoBytes * 8 > 1023) {
1347
+ throw new MyStarsValidationError(
1348
+ `memo is ${memoBytes} UTF-8 bytes; max ${MAX_MEMO_BYTES} fit in a single-cell op-0 forward payload`
1349
+ );
1350
+ }
1351
+ const refBuilder = new BitBuilder();
1352
+ refBuilder.storeUint(0, 32).storeStringTail(memo);
1353
+ const refCell = refBuilder.finalize();
1354
+ const serializedRef = serializeCell(refCell.data, refCell.bits, []);
1355
+ const bodyBuilder = new BitBuilder();
1356
+ bodyBuilder.storeUint(JETTON_TRANSFER_OP, 32).storeUint(0, 64).storeCoins(BigInt(amountMicro)).storeAddress(destination).storeAddress(sender).storeBit(0).storeCoins(FORWARD_TON_AMOUNT_NANO).storeBit(1);
1357
+ const bodyCell = bodyBuilder.finalize();
1358
+ const serializedBody = serializeCell(bodyCell.data, bodyCell.bits, [1]);
1359
+ return serializeBoC([serializedBody, serializedRef]);
1360
+ }
1361
+
1362
+ // src/payment/invoice.ts
1363
+ var JETTON_TRANSFER_GAS_NANO = "50000000";
1364
+ var DECIMAL_RE = /^\d+(\.\d+)?$/;
1365
+ function decimalToUnits(amount, decimals) {
1366
+ if (amount.trimStart().startsWith("-")) {
1367
+ throw new MyStarsValidationError(`amount must not be negative, got "${amount}"`);
1368
+ }
1369
+ if (!DECIMAL_RE.test(amount)) throw new MyStarsValidationError(`invalid decimal amount "${amount}"`);
1370
+ const [intPart, fracPart = ""] = amount.split(".");
1371
+ const scaledFrac = fracPart.padEnd(decimals + 1, "0");
1372
+ const keep = scaledFrac.slice(0, decimals);
1373
+ const roundDigit = scaledFrac[decimals];
1374
+ let units = BigInt((intPart || "0") + keep);
1375
+ if (roundDigit !== void 0 && Number(roundDigit) >= 5) units += 1n;
1376
+ return units;
1377
+ }
1378
+ function toNano(amount) {
1379
+ return decimalToUnits(amount, 9);
1380
+ }
1381
+ function toMicro(amount) {
1382
+ return decimalToUnits(amount, 6);
1383
+ }
1384
+ function requireAddress(p) {
1385
+ if (!p.pay_to_address) throw new MyStarsValidationError("payment.pay_to_address is missing");
1386
+ return p.pay_to_address;
1387
+ }
1388
+ function requireMemo(p) {
1389
+ if (!p.memo) throw new MyStarsValidationError("payment.memo is missing");
1390
+ return p.memo;
1391
+ }
1392
+ function requirePositiveUnits(p) {
1393
+ const units = p.currency === "ton" ? toNano(p.amount) : toMicro(p.amount);
1394
+ if (units <= 0n) {
1395
+ throw new MyStarsValidationError(`payment amount must be positive, got "${p.amount}"`);
1396
+ }
1397
+ return units;
1398
+ }
1399
+ function buildTonConnectMessages(payment, opts = {}) {
1400
+ const payTo = requireAddress(payment);
1401
+ const memo = requireMemo(payment);
1402
+ requirePositiveUnits(payment);
1403
+ if (payment.currency === "ton") {
1404
+ return [{ address: payTo, amount: toNano(payment.amount).toString(), payload: buildCommentPayload(memo) }];
1405
+ }
1406
+ if (!opts.senderAddress || !opts.jettonWalletAddress) {
1407
+ throw new MyStarsValidationError(
1408
+ "a USDT TON Connect message needs senderAddress + jettonWalletAddress (the payer's own USDT jetton wallet)"
1409
+ );
1410
+ }
1411
+ const payload = buildJettonTransferPayload(toMicro(payment.amount), payTo, opts.senderAddress, memo);
1412
+ return [{ address: opts.jettonWalletAddress, amount: JETTON_TRANSFER_GAS_NANO, payload }];
1413
+ }
1414
+ function buildTonDeeplink(payment) {
1415
+ if (payment.currency !== "ton") {
1416
+ throw new MyStarsValidationError("a ton:// deeplink is only valid for `ton` payments (USDT needs a jetton transfer)");
1417
+ }
1418
+ const payTo = requireAddress(payment);
1419
+ const memo = requireMemo(payment);
1420
+ requirePositiveUnits(payment);
1421
+ const nano = toNano(payment.amount).toString();
1422
+ return `ton://transfer/${payTo}?amount=${nano}&text=${encodeURIComponent(memo)}`;
1423
+ }
1424
+ function buildPaymentRequest(payment, opts = {}) {
1425
+ const payTo = requireAddress(payment);
1426
+ const memo = requireMemo(payment);
1427
+ requirePositiveUnits(payment);
1428
+ if (payment.currency === "ton") {
1429
+ const nano = toNano(payment.amount).toString();
1430
+ const deeplink = `ton://transfer/${payTo}?amount=${nano}&text=${encodeURIComponent(memo)}`;
1431
+ return {
1432
+ currency: "ton",
1433
+ payToAddress: payTo,
1434
+ memo,
1435
+ amountUnits: "ton",
1436
+ amountSmallestUnit: nano,
1437
+ tonDeeplink: deeplink,
1438
+ tonkeeperLink: `https://app.tonkeeper.com/transfer/${payTo}?amount=${nano}&text=${encodeURIComponent(memo)}`,
1439
+ qrPayload: deeplink,
1440
+ tonConnect: [{ address: payTo, amount: nano, payload: buildCommentPayload(memo) }]
1441
+ };
1442
+ }
1443
+ const micro = toMicro(payment.amount).toString();
1444
+ const base = {
1445
+ currency: "usdt_ton",
1446
+ payToAddress: payTo,
1447
+ memo,
1448
+ amountUnits: "usdt",
1449
+ amountSmallestUnit: micro,
1450
+ tonConnect: []
1451
+ };
1452
+ if (opts.senderAddress && opts.jettonWalletAddress) {
1453
+ base.tonConnect = buildTonConnectMessages(payment, opts);
1454
+ } else {
1455
+ base.note = "USDT: pass senderAddress + jettonWalletAddress (the payer's own USDT jetton wallet) to build a signable TON Connect message.";
1456
+ }
1457
+ return base;
1458
+ }
1459
+
1460
+ export { BadRequestError, CANCELLABLE_STATUSES, CONTRACT_VERSION, ConflictError, FORWARD_TON_AMOUNT_NANO, ForbiddenError, INITIAL_ORDER_STATUS, IdempotencyConflictError, InternalServerError, JETTON_TRANSFER_GAS_NANO, JETTON_TRANSFER_OP, MyStarsApiError, MyStarsClient, MyStarsError, MyStarsValidationError, NetworkError, NotFoundError, OrderNotCancellableError, OrderWaitTimeoutError, OrdersPager, PREMIUM_MONTHS, PRODUCTION_BASE_URL, RateLimitError, RecipientIneligibleError, SDK_VERSION, STARS_MAX_QUANTITY, STARS_MIN_QUANTITY, ServiceUnavailableError, TERMINAL_STATUSES, TimeoutError, UnauthorizedError, WEBHOOK_TERMINAL, WebhookSignatureError, applyRetailMarkup, buildCommentPayload, buildJettonTransferPayload, buildPaymentRequest, buildTonConnectMessages, buildTonDeeplink, canonicalUsername, ceilTonTo4dp, ceilUsdToCents, constructEvent, decimalToUnits, defaultShouldRetry, errorFromResponse, expressWebhook, fastifyWebhook, isTerminal, parseRetryAfterMs, parseTonAddress, reconcile, toMicro, toNano, verifyWebhookSignature };