@skalpgg/sdk 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ Create the key on the API keys page of your dashboard. Each key is bound to one
47
47
  | `orders.get(id)` | `orders:read` |
48
48
  | `orders.cancel(id)` | `orders:write` |
49
49
  | `orders.replace(id, { serialIds?, values?, reason? })` | `orders:write` |
50
- | `products.list({ status? })` | `products:read` |
50
+ | `products.list({ status?, limit?, offset? })` | `products:read` |
51
51
  | `products.get(idOrSlug)` | `products:read` |
52
52
  | `products.create({ name, price, fulfillmentType?, submit?, ... })` | `products:write` |
53
53
  | `products.update(idOrSlug, { price?, status?: 'DRAFT' \| 'ARCHIVED', ... })` | `products:write` |
@@ -65,13 +65,12 @@ Create the key on the API keys page of your dashboard. Each key is bound to one
65
65
  | `coupons.create({ code, discountType?, discountPercent?, discountAmount?, validFrom?, validTo?, maxUses?, ... })` / `coupons.update(id, { ... })` / `coupons.delete(id)` | `coupons:write` |
66
66
  | `customers.list({ search?, limit?, offset? })` / `customers.get(id)` | `customers:read` |
67
67
  | `customers.create({ email, name?, note? })` / `customers.update(id, { name?, note? })` / `customers.delete(id)` | `customers:write` |
68
- | `customers.adjustBalance(id, { amount, reason? })` | `customers:write` |
69
68
  | `feedback.list({ productId?, rating?, limit?, offset? })` / `feedback.get(id)` | `feedback:read` |
70
69
  | `feedback.reply(id, { message })` / `feedback.appeal(id, { reason })` | `feedback:write` |
71
70
  | `queries.list({ status?, limit?, offset? })` / `queries.get(id)` | `queries:read` |
72
71
  | `queries.reply(id, { message })` / `queries.close(id)` / `queries.reopen(id)` | `queries:write` |
73
- | `blacklist.list({ type? })` / `whitelist.list({ type? })` | `blacklist:read` |
74
- | `blacklist.add({ type, value, note? })` / `whitelist.add({ ... })` / `blacklist.remove(id)` / `whitelist.remove(id)` | `blacklist:write` |
72
+ | `blacklist.list({ type? })` / `blacklist.add({ type, value, note? })` / `blacklist.remove(id)` | `blacklist:read` / `blacklist:write` |
73
+ | `whitelist.list({ type? })` / `whitelist.add({ type, value, note? })` / `whitelist.remove(id)` | `whitelist:read` / `whitelist:write` |
75
74
  | `subscriptions.createCheckout({ productId \| productSlug, externalRef?, customerEmail?, currency?, ... })` | `checkout:write` |
76
75
  | `subscriptions.getCheckout(id)` | `checkout:read` |
77
76
  | `subscriptions.list({ externalRef?, status?, updatedSince?, limit?, offset? })` | `subscriptions:read` |
@@ -119,19 +118,17 @@ const { data: orders } = await skalp.orders.list({ status: 'COMPLETED', updatedS
119
118
  await skalp.orders.cancel(order.id)
120
119
 
121
120
  const { replacements } = await skalp.orders.replace(order.id, { reason: 'Key does not activate' })
122
-
123
- await skalp.customers.adjustBalance(customer.id, { amount: 10, reason: 'Goodwill credit' })
124
121
  ```
125
122
 
126
- `cancel` works only on a `PENDING` order and returns it as `CANCELED`. `replace` swaps the delivered keys of a completed order: with no `serialIds` it replaces every key, and with no `values` it draws the new ones from stock (`values` is required for keys that come from your server). The replacement keys come back in `replacements[].value`. `orders.get` also returns `serialsReplaced` and the `replacements` history.
123
+ `cancel` works only on a `PENDING` order and returns it as `CANCELED`. `replace` swaps the delivered keys of a completed order: with no `serialIds` it replaces every key, and with no `values` it draws the new ones from stock (`values` is required for keys that come from your server). The replacement keys come back in `replacements[].value`. `orders.get` also returns `serialsReplaced`, the `replacements` history and `serials`: the id, status, `deliveredAt` and `replacedById` of every delivered or replaced key (never its value), which are the ids `serialIds` expects.
127
124
 
128
- `updatedSince` lists oldest change first, so you can page through it and keep the last `updatedAt` as your cursor; without it, newest orders come first. Amounts are in major units (`19.5`), balances cannot go below zero, and a customer with a balance cannot be deleted.
125
+ `updatedSince` lists oldest change first, so you can page through it and keep the last `updatedAt` as your cursor; without it, newest orders come first. Amounts are in major units (`19.5`).
129
126
 
130
127
  Coupons take `validFrom`, `validTo` as an ISO string or a `Date`; `coupons.update` accepts `null` to clear `validTo`, `maxUses`, `minPurchaseAmount` and `description`.
131
128
 
132
129
  ## Support and access rules
133
130
 
134
- `feedback` and `queries` expose product reviews and buyer conversations. `queries.reply`, `close` and `reopen` return the whole conversation with its `messages`. `blacklist` and `whitelist` use the same calls and the same `blacklist:read` and `blacklist:write` scopes; a whitelisted buyer is never blocked by a blacklist rule.
131
+ `feedback` and `queries` expose product reviews and buyer conversations. `queries.reply`, `close` and `reopen` return the whole conversation with its `messages`. `blacklist` and `whitelist` use the same calls, each with its own read and write scope; a whitelisted buyer is never blocked by a blacklist rule.
135
132
 
136
133
  ```ts
137
134
  await skalp.blacklist.add({ type: 'EMAIL', value: 'chargeback@example.com', note: 'Disputed order 1042' })
@@ -174,7 +171,7 @@ export async function POST(request: Request) {
174
171
 
175
172
  A webhook with the default events receives every purchase as both `order.paid` and its legacy name `purchase.completed`. Handle `order.paid` and deduplicate on `data.idempotencyKey`.
176
173
 
177
- `order.created`, `order.updated`, `order.canceled`, `order.replaced`, `query.created`, `query.replied`, `feedback.received` and the subscription events `subscription.updated`, `subscription.trial_started`, `subscription.trial_ended` and `subscription.upcoming` are not in the default list either: subscribe the webhook to them by name, and `event.type` narrows `event.data` to the right payload. `order.created`, `order.updated` and `order.canceled` carry the order as `orders.get` returns it, plus `previousStatus`; `order.replaced` adds `reason` and the new keys in `replacements[].key`. The three trial and upcoming events are accepted by the API but the backend does not send them yet.
174
+ `order.created`, `order.updated`, `order.canceled`, `order.replaced`, `query.created`, `query.replied`, `feedback.received` and the subscription events `subscription.updated`, `subscription.trial_started`, `subscription.trial_ended` and `subscription.upcoming` are not in the default list either: subscribe the webhook to them by name, and `event.type` narrows `event.data` to the right payload. `order.created`, `order.updated` and `order.canceled` carry the order as `orders.get` returns it, plus `previousStatus`; `order.replaced` adds `reason` and the new keys in `replacements[].key`. Every one of them, like `query.created`, `query.replied` and `feedback.received`, carries a `data.idempotencyKey` that stays the same on every retry of the same event, so deduplicate on it. The three trial and upcoming events are accepted by the API but the backend does not send them yet.
178
175
 
179
176
  `product.created`, `product.updated` and `product.stock` are not in the default list: subscribe the webhook to them by name. `product.stock` carries `stock`, `previousStock` and `change` whenever the number of available keys changes.
180
177
 
@@ -1,6 +1,6 @@
1
- import type { AccessRule, AddAccessRuleParams, AddSerialsParams, AddSerialsResult, AdjustCustomerBalanceParams, AppealFeedbackParams, ChangeSubscriptionPlanParams, CheckoutSession, CheckoutSessionStatus, CoinCheckout, CoinCheckoutStatus, Coupon, CreateCheckoutParams, CreateCoinCheckoutParams, CreateCouponParams, CreateCustomerParams, CreateProductGroupParams, CreateProductParams, CreateStoreCategoryParams, CreateSubscriptionCheckoutParams, CreateUsageChargeParams, Customer, CustomerDetails, CustomerList, Deleted, Feedback, FeedbackList, HeartbeatParams, Invoice, LicenseKeyInfo, LicenseValidation, LicenseVerification, ListAccessRulesParams, ListCustomersParams, ListFeedbackParams, ListOrdersParams, ListProductsParams, ListQueriesParams, ListSerialsParams, ListSubscriptionsParams, Order, OrderKeysReplaced, OrderList, Product, ProductGroup, ProductStock, ReplaceOrderKeysParams, ReplyParams, SerialList, SkalpEvent, StoreCategory, StoreCategoryWithCount, Subscription, SubscriptionCheckout, SubscriptionCheckoutStatus, SubscriptionList, SupportQueryDetails, SupportQueryList, ResetLicenseDevicesParams, UpdateCouponParams, UpdateCustomerParams, UpdateProductGroupParams, UpdateProductParams, UpdateStoreCategoryParams, UsageCharge, ValidateLicenseParams, VerifyLicenseParams } from './types.js';
1
+ import type { AccessRule, AddAccessRuleParams, AddSerialsParams, AddSerialsResult, AppealFeedbackParams, ChangeSubscriptionPlanParams, CheckoutSession, CheckoutSessionStatus, CoinCheckout, CoinCheckoutStatus, Coupon, CreateCheckoutParams, CreateCoinCheckoutParams, CreateCouponParams, CreateCustomerParams, CreateProductGroupParams, CreateProductParams, CreateStoreCategoryParams, CreateSubscriptionCheckoutParams, CreateUsageChargeParams, Customer, CustomerDetails, CustomerList, Deleted, Feedback, FeedbackList, HeartbeatParams, Invoice, LicenseKeyInfo, LicenseValidation, LicenseVerification, ListAccessRulesParams, ListCustomersParams, ListFeedbackParams, ListOrdersParams, ListProductsParams, ListQueriesParams, ListSerialsParams, ListSubscriptionsParams, Order, OrderKeysReplaced, OrderList, Product, ProductGroup, ProductStock, ReplaceOrderKeysParams, ReplyParams, SerialList, SkalpEvent, StoreCategory, StoreCategoryWithCount, Subscription, SubscriptionCheckout, SubscriptionCheckoutStatus, SubscriptionList, SupportQueryDetails, SupportQueryList, ResetLicenseDevicesParams, UpdateCouponParams, UpdateCustomerParams, UpdateProductGroupParams, UpdateProductParams, UpdateStoreCategoryParams, UsageCharge, ValidateLicenseParams, VerifyLicenseParams } from './types.js';
2
2
  export type * from './types.js';
3
- export declare const VERSION = "1.3.0";
3
+ export declare const VERSION = "1.5.0";
4
4
  export interface SkalpOptions {
5
5
  apiKey?: string;
6
6
  webhookSecret?: string;
@@ -42,7 +42,7 @@ export declare class Skalp {
42
42
  replace: (id: string, { serialIds, values, reason }?: ReplaceOrderKeysParams) => Promise<OrderKeysReplaced>;
43
43
  };
44
44
  readonly products: {
45
- list: ({ status }?: ListProductsParams) => Promise<Product[]>;
45
+ list: ({ status, limit, offset }?: ListProductsParams) => Promise<Array<Product>>;
46
46
  get: (idOrSlug: string) => Promise<Product>;
47
47
  create: (params: CreateProductParams) => Promise<Product>;
48
48
  update: (idOrSlug: string, params: UpdateProductParams) => Promise<Product>;
@@ -79,7 +79,6 @@ export declare class Skalp {
79
79
  create: ({ email, name, note }: CreateCustomerParams) => Promise<Customer>;
80
80
  update: (id: string, { name, note }: UpdateCustomerParams) => Promise<Customer>;
81
81
  delete: (id: string) => Promise<Deleted>;
82
- adjustBalance: (id: string, { amount, reason }: AdjustCustomerBalanceParams) => Promise<Customer>;
83
82
  };
84
83
  readonly feedback: {
85
84
  list: ({ productId, rating, limit, offset }?: ListFeedbackParams) => Promise<FeedbackList>;
package/dist/cjs/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Skalp = exports.SkalpSignatureError = exports.SkalpConnectionError = exports.SkalpError = exports.VERSION = void 0;
4
- exports.VERSION = '1.3.0';
4
+ exports.VERSION = '1.5.0';
5
5
  const SIGNATURE_HEADER = 'skalp-signature';
6
6
  const SIGNATURE_TOLERANCE_SECONDS = 300;
7
+ const PRODUCT_PAGE = 100;
7
8
  const USER_AGENT = `skalp-node/${exports.VERSION}`;
8
9
  const DEFAULT_TIMEOUT_MS = 30_000;
9
10
  const MAX_RETRY_WAIT_SECONDS = 10;
@@ -128,7 +129,21 @@ class Skalp {
128
129
  }),
129
130
  };
130
131
  products = {
131
- list: ({ status } = {}) => this.#request('GET', '/v1/products', { query: { status } }),
132
+ list: async ({ status, limit, offset } = {}) => {
133
+ if (limit !== undefined || offset !== undefined) {
134
+ return (await this.#send('GET', '/v1/products', { query: { status, limit, offset } })).data;
135
+ }
136
+ const products = [];
137
+ for (;;) {
138
+ const page = await this.#send('GET', '/v1/products', {
139
+ query: { status, limit: PRODUCT_PAGE, offset: products.length },
140
+ });
141
+ products.push(...page.data);
142
+ const total = Number(page.headers.get('x-total-count'));
143
+ if (page.data.length < PRODUCT_PAGE || products.length >= total)
144
+ return products;
145
+ }
146
+ },
132
147
  get: (idOrSlug) => this.#request('GET', `/v1/products/${segment(idOrSlug)}`),
133
148
  create: (params) => this.#request('POST', '/v1/products', {
134
149
  body: pick(params, [...PRODUCT_FIELDS, 'productType', 'submit']),
@@ -185,7 +200,6 @@ class Skalp {
185
200
  create: ({ email, name, note }) => this.#request('POST', '/v1/customers', { body: { email, name, note } }),
186
201
  update: (id, { name, note }) => this.#request('PATCH', `/v1/customers/${segment(id)}`, { body: { name, note } }),
187
202
  delete: (id) => this.#request('DELETE', `/v1/customers/${segment(id)}`),
188
- adjustBalance: (id, { amount, reason }) => this.#request('POST', `/v1/customers/${segment(id)}/balance`, { body: { amount, reason } }),
189
203
  };
190
204
  feedback = {
191
205
  list: ({ productId, rating, limit, offset } = {}) => this.#request('GET', '/v1/feedback', { query: { productId, rating, limit, offset } }),
@@ -295,7 +309,10 @@ class Skalp {
295
309
  const hmac = await crypto.subtle.importKey('raw', encoder.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
296
310
  return (await crypto.subtle.verify('HMAC', hmac, parsed.signature, signed)) ? undefined : 'invalid';
297
311
  }
298
- async #request(method, path, { body, query, auth = true } = {}) {
312
+ async #request(method, path, options = {}) {
313
+ return (await this.#send(method, path, options)).data;
314
+ }
315
+ async #send(method, path, { body, query, auth = true } = {}) {
299
316
  if (auth && !this.#apiKey) {
300
317
  throw new Error('Skalp: missing API key. Pass it to new Skalp(apiKey) or set SKALP_API_KEY.');
301
318
  }
@@ -340,7 +357,7 @@ class Skalp {
340
357
  }
341
358
  if (!res.ok)
342
359
  throw toError(res.status, data);
343
- return data;
360
+ return { data: data, headers: res.headers };
344
361
  }
345
362
  }
346
363
  }
@@ -60,6 +60,8 @@ export interface Order {
60
60
  deliveryStatus: DeliveryStatus | null;
61
61
  serialsCount?: number;
62
62
  serialsReplaced?: number;
63
+ /** Delivered and replaced keys by id, never their values. Only orders.get and orders.cancel fill it in. */
64
+ serials?: Array<OrderSerial>;
63
65
  createdAt: string;
64
66
  updatedAt: string;
65
67
  completedAt: string | null;
@@ -68,6 +70,14 @@ export interface Order {
68
70
  items?: Array<OrderItem>;
69
71
  replacements?: Array<OrderReplacement>;
70
72
  }
73
+ export interface OrderSerial {
74
+ id: string;
75
+ productId: string;
76
+ status: 'DELIVERED' | 'REPLACED';
77
+ deliveredAt: string | null;
78
+ /** The serial that took this one's place, once replaced. */
79
+ replacedById: string | null;
80
+ }
71
81
  export interface OrderReplacement {
72
82
  id: string;
73
83
  productId: string;
@@ -195,6 +205,13 @@ export interface Product {
195
205
  deliveryType: DeliveryType | null;
196
206
  /** Server stores: what the game server runs after payment. */
197
207
  deliveryPayload: string | null;
208
+ revokePayload: string | null;
209
+ coinConfig: CoinConfig | null;
210
+ pendingChanges: {
211
+ submittedAt: string;
212
+ changes: Partial<Record<ReviewedProductField, unknown>>;
213
+ } | null;
214
+ pendingChangesRejection: string | null;
198
215
  visibility: ProductVisibility;
199
216
  /** Higher comes first on the store page. */
200
217
  sortPriority: number;
@@ -222,8 +239,24 @@ export interface Product {
222
239
  createdAt: string;
223
240
  updatedAt: string;
224
241
  }
242
+ export interface CoinBonusTier {
243
+ thresholdCoins: number;
244
+ bonusPercent: number;
245
+ }
246
+ export interface CoinConfig {
247
+ unitPriceCents: number;
248
+ minQty: number;
249
+ maxQty: number;
250
+ step: number;
251
+ currency: string;
252
+ bonusTiers: Array<CoinBonusTier>;
253
+ }
254
+ export type CoinConfigInput = Omit<CoinConfig, 'currency'>;
255
+ export type ReviewedProductField = 'name' | 'shortDescription' | 'description' | 'images' | 'videoUrl' | 'features' | 'requirements' | 'customFields' | 'deliveryNote' | 'termsOfService' | 'warrantyText' | 'deliveryType' | 'deliveryPayload' | 'revokePayload';
225
256
  export interface ListProductsParams {
226
257
  status?: ProductStatus | 'all';
258
+ limit?: number;
259
+ offset?: number;
227
260
  }
228
261
  interface ProductFields {
229
262
  slug?: string;
@@ -245,6 +278,8 @@ interface ProductFields {
245
278
  deliveryType?: DeliveryType;
246
279
  /** Required for a server store; needs a placeholder such as {steamhex}. */
247
280
  deliveryPayload?: string;
281
+ revokePayload?: string;
282
+ coinConfig?: CoinConfigInput;
248
283
  visibility?: ProductVisibility;
249
284
  sortPriority?: number;
250
285
  /** null or an empty string clears it. */
@@ -269,7 +304,7 @@ export interface CreateProductParams extends ProductFields {
269
304
  name: string;
270
305
  price: number;
271
306
  comparePrice?: number;
272
- productType?: 'ONE_TIME' | 'RECURRING';
307
+ productType?: 'ONE_TIME' | 'RECURRING' | 'COINS';
273
308
  submit?: boolean;
274
309
  }
275
310
  export interface UpdateProductParams extends ProductFields {
@@ -427,7 +462,6 @@ export interface Customer {
427
462
  name: string | null;
428
463
  note: string | null;
429
464
  country: string | null;
430
- balance: number;
431
465
  ordersCount: number;
432
466
  totalSpent: number;
433
467
  currency: Currency;
@@ -436,17 +470,8 @@ export interface Customer {
436
470
  createdAt: string;
437
471
  updatedAt: string;
438
472
  }
439
- export interface CustomerBalanceEntry {
440
- id: string;
441
- amount: number;
442
- balanceAfter: number;
443
- reason: string | null;
444
- orderId: string | null;
445
- createdAt: string;
446
- }
447
473
  export interface CustomerDetails extends Customer {
448
474
  recentOrders: Array<Order>;
449
- balanceHistory: Array<CustomerBalanceEntry>;
450
475
  }
451
476
  export interface ListCustomersParams {
452
477
  search?: string;
@@ -468,10 +493,6 @@ export interface UpdateCustomerParams {
468
493
  name?: string | null;
469
494
  note?: string | null;
470
495
  }
471
- export interface AdjustCustomerBalanceParams {
472
- amount: number;
473
- reason?: string;
474
- }
475
496
  export interface FeedbackReply {
476
497
  id: string;
477
498
  content: string;
@@ -803,6 +824,16 @@ export interface OrderRefundedData {
803
824
  granted?: boolean;
804
825
  fullRefund: boolean;
805
826
  licenses: Array<WebhookLicense>;
827
+ revocations: Array<Revocation>;
828
+ }
829
+ export interface Revocation {
830
+ productId: string;
831
+ productSlug: string;
832
+ productName: string;
833
+ deliveryKey: string;
834
+ delivered: boolean;
835
+ deliveryType: DeliveryType;
836
+ revokeCommand: string;
806
837
  }
807
838
  export interface OrderDisputedData {
808
839
  orderId: string;
@@ -869,19 +900,20 @@ export interface WebhookTestData {
869
900
  webhookId: string;
870
901
  sample: Partial<OrderPaidData>;
871
902
  }
872
- export type OrderEventData = Omit<Order, 'serialsCount' | 'serialsReplaced' | 'replacements'> & {
903
+ export type OrderEventData = Omit<Order, 'serialsCount' | 'serialsReplaced' | 'serials' | 'replacements'> & {
873
904
  previousStatus: OrderStatus | null;
905
+ /** Dedupe key: `order.created:<orderId>`, `order.updated:<orderId>:<transition>` or `order.canceled:<orderId>`. */
906
+ idempotencyKey: string;
874
907
  };
875
- export interface OrderReplacedData extends Omit<Order, 'serialsCount' | 'serialsReplaced' | 'replacements'> {
908
+ export interface OrderReplacedData extends Omit<Order, 'serialsCount' | 'serialsReplaced' | 'serials' | 'replacements'> {
909
+ /** Dedupe key: `order.replaced:<orderId>:<first new serial id>`. */
910
+ idempotencyKey: string;
876
911
  reason: string | null;
877
- replacements: Array<{
878
- productId: string;
879
- oldSerialId: string;
880
- newSerialId: string;
881
- key: string;
882
- }>;
912
+ replacements: Array<OrderKeyReplacement>;
883
913
  }
884
914
  export interface QueryEventData {
915
+ /** Dedupe key: `query.created:<queryId>` or `query.replied:<messageId>`. */
916
+ idempotencyKey: string;
885
917
  queryId: string;
886
918
  title: string;
887
919
  status: QueryStatus;
@@ -895,6 +927,8 @@ export interface QueryEventData {
895
927
  };
896
928
  }
897
929
  export interface FeedbackReceivedData {
930
+ /** Dedupe key: `feedback.received:<feedbackId>`. */
931
+ idempotencyKey: string;
898
932
  feedbackId: string;
899
933
  productId: string;
900
934
  productName: string;
@@ -915,6 +949,7 @@ export type SkalpEvent = Envelope<'order.created' | 'order.updated' | 'order.can
915
949
  }> | Envelope<'subscription.canceled', SubscriptionEventData & {
916
950
  endedAt: string | null;
917
951
  cancelReason: 'requested' | 'payment_failure' | 'dispute' | 'stripe';
952
+ revokeCommand?: string;
918
953
  }> | Envelope<'subscription.refunded', SubscriptionEventData & {
919
954
  invoiceId: string;
920
955
  amountRefunded: number;
@@ -1,6 +1,6 @@
1
- import type { AccessRule, AddAccessRuleParams, AddSerialsParams, AddSerialsResult, AdjustCustomerBalanceParams, AppealFeedbackParams, ChangeSubscriptionPlanParams, CheckoutSession, CheckoutSessionStatus, CoinCheckout, CoinCheckoutStatus, Coupon, CreateCheckoutParams, CreateCoinCheckoutParams, CreateCouponParams, CreateCustomerParams, CreateProductGroupParams, CreateProductParams, CreateStoreCategoryParams, CreateSubscriptionCheckoutParams, CreateUsageChargeParams, Customer, CustomerDetails, CustomerList, Deleted, Feedback, FeedbackList, HeartbeatParams, Invoice, LicenseKeyInfo, LicenseValidation, LicenseVerification, ListAccessRulesParams, ListCustomersParams, ListFeedbackParams, ListOrdersParams, ListProductsParams, ListQueriesParams, ListSerialsParams, ListSubscriptionsParams, Order, OrderKeysReplaced, OrderList, Product, ProductGroup, ProductStock, ReplaceOrderKeysParams, ReplyParams, SerialList, SkalpEvent, StoreCategory, StoreCategoryWithCount, Subscription, SubscriptionCheckout, SubscriptionCheckoutStatus, SubscriptionList, SupportQueryDetails, SupportQueryList, ResetLicenseDevicesParams, UpdateCouponParams, UpdateCustomerParams, UpdateProductGroupParams, UpdateProductParams, UpdateStoreCategoryParams, UsageCharge, ValidateLicenseParams, VerifyLicenseParams } from './types.js';
1
+ import type { AccessRule, AddAccessRuleParams, AddSerialsParams, AddSerialsResult, AppealFeedbackParams, ChangeSubscriptionPlanParams, CheckoutSession, CheckoutSessionStatus, CoinCheckout, CoinCheckoutStatus, Coupon, CreateCheckoutParams, CreateCoinCheckoutParams, CreateCouponParams, CreateCustomerParams, CreateProductGroupParams, CreateProductParams, CreateStoreCategoryParams, CreateSubscriptionCheckoutParams, CreateUsageChargeParams, Customer, CustomerDetails, CustomerList, Deleted, Feedback, FeedbackList, HeartbeatParams, Invoice, LicenseKeyInfo, LicenseValidation, LicenseVerification, ListAccessRulesParams, ListCustomersParams, ListFeedbackParams, ListOrdersParams, ListProductsParams, ListQueriesParams, ListSerialsParams, ListSubscriptionsParams, Order, OrderKeysReplaced, OrderList, Product, ProductGroup, ProductStock, ReplaceOrderKeysParams, ReplyParams, SerialList, SkalpEvent, StoreCategory, StoreCategoryWithCount, Subscription, SubscriptionCheckout, SubscriptionCheckoutStatus, SubscriptionList, SupportQueryDetails, SupportQueryList, ResetLicenseDevicesParams, UpdateCouponParams, UpdateCustomerParams, UpdateProductGroupParams, UpdateProductParams, UpdateStoreCategoryParams, UsageCharge, ValidateLicenseParams, VerifyLicenseParams } from './types.js';
2
2
  export type * from './types.js';
3
- export declare const VERSION = "1.3.0";
3
+ export declare const VERSION = "1.5.0";
4
4
  export interface SkalpOptions {
5
5
  apiKey?: string;
6
6
  webhookSecret?: string;
@@ -42,7 +42,7 @@ export declare class Skalp {
42
42
  replace: (id: string, { serialIds, values, reason }?: ReplaceOrderKeysParams) => Promise<OrderKeysReplaced>;
43
43
  };
44
44
  readonly products: {
45
- list: ({ status }?: ListProductsParams) => Promise<Product[]>;
45
+ list: ({ status, limit, offset }?: ListProductsParams) => Promise<Array<Product>>;
46
46
  get: (idOrSlug: string) => Promise<Product>;
47
47
  create: (params: CreateProductParams) => Promise<Product>;
48
48
  update: (idOrSlug: string, params: UpdateProductParams) => Promise<Product>;
@@ -79,7 +79,6 @@ export declare class Skalp {
79
79
  create: ({ email, name, note }: CreateCustomerParams) => Promise<Customer>;
80
80
  update: (id: string, { name, note }: UpdateCustomerParams) => Promise<Customer>;
81
81
  delete: (id: string) => Promise<Deleted>;
82
- adjustBalance: (id: string, { amount, reason }: AdjustCustomerBalanceParams) => Promise<Customer>;
83
82
  };
84
83
  readonly feedback: {
85
84
  list: ({ productId, rating, limit, offset }?: ListFeedbackParams) => Promise<FeedbackList>;
package/dist/esm/index.js CHANGED
@@ -1,6 +1,7 @@
1
- export const VERSION = '1.3.0';
1
+ export const VERSION = '1.5.0';
2
2
  const SIGNATURE_HEADER = 'skalp-signature';
3
3
  const SIGNATURE_TOLERANCE_SECONDS = 300;
4
+ const PRODUCT_PAGE = 100;
4
5
  const USER_AGENT = `skalp-node/${VERSION}`;
5
6
  const DEFAULT_TIMEOUT_MS = 30_000;
6
7
  const MAX_RETRY_WAIT_SECONDS = 10;
@@ -122,7 +123,21 @@ export class Skalp {
122
123
  }),
123
124
  };
124
125
  products = {
125
- list: ({ status } = {}) => this.#request('GET', '/v1/products', { query: { status } }),
126
+ list: async ({ status, limit, offset } = {}) => {
127
+ if (limit !== undefined || offset !== undefined) {
128
+ return (await this.#send('GET', '/v1/products', { query: { status, limit, offset } })).data;
129
+ }
130
+ const products = [];
131
+ for (;;) {
132
+ const page = await this.#send('GET', '/v1/products', {
133
+ query: { status, limit: PRODUCT_PAGE, offset: products.length },
134
+ });
135
+ products.push(...page.data);
136
+ const total = Number(page.headers.get('x-total-count'));
137
+ if (page.data.length < PRODUCT_PAGE || products.length >= total)
138
+ return products;
139
+ }
140
+ },
126
141
  get: (idOrSlug) => this.#request('GET', `/v1/products/${segment(idOrSlug)}`),
127
142
  create: (params) => this.#request('POST', '/v1/products', {
128
143
  body: pick(params, [...PRODUCT_FIELDS, 'productType', 'submit']),
@@ -179,7 +194,6 @@ export class Skalp {
179
194
  create: ({ email, name, note }) => this.#request('POST', '/v1/customers', { body: { email, name, note } }),
180
195
  update: (id, { name, note }) => this.#request('PATCH', `/v1/customers/${segment(id)}`, { body: { name, note } }),
181
196
  delete: (id) => this.#request('DELETE', `/v1/customers/${segment(id)}`),
182
- adjustBalance: (id, { amount, reason }) => this.#request('POST', `/v1/customers/${segment(id)}/balance`, { body: { amount, reason } }),
183
197
  };
184
198
  feedback = {
185
199
  list: ({ productId, rating, limit, offset } = {}) => this.#request('GET', '/v1/feedback', { query: { productId, rating, limit, offset } }),
@@ -289,7 +303,10 @@ export class Skalp {
289
303
  const hmac = await crypto.subtle.importKey('raw', encoder.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
290
304
  return (await crypto.subtle.verify('HMAC', hmac, parsed.signature, signed)) ? undefined : 'invalid';
291
305
  }
292
- async #request(method, path, { body, query, auth = true } = {}) {
306
+ async #request(method, path, options = {}) {
307
+ return (await this.#send(method, path, options)).data;
308
+ }
309
+ async #send(method, path, { body, query, auth = true } = {}) {
293
310
  if (auth && !this.#apiKey) {
294
311
  throw new Error('Skalp: missing API key. Pass it to new Skalp(apiKey) or set SKALP_API_KEY.');
295
312
  }
@@ -334,7 +351,7 @@ export class Skalp {
334
351
  }
335
352
  if (!res.ok)
336
353
  throw toError(res.status, data);
337
- return data;
354
+ return { data: data, headers: res.headers };
338
355
  }
339
356
  }
340
357
  }
@@ -60,6 +60,8 @@ export interface Order {
60
60
  deliveryStatus: DeliveryStatus | null;
61
61
  serialsCount?: number;
62
62
  serialsReplaced?: number;
63
+ /** Delivered and replaced keys by id, never their values. Only orders.get and orders.cancel fill it in. */
64
+ serials?: Array<OrderSerial>;
63
65
  createdAt: string;
64
66
  updatedAt: string;
65
67
  completedAt: string | null;
@@ -68,6 +70,14 @@ export interface Order {
68
70
  items?: Array<OrderItem>;
69
71
  replacements?: Array<OrderReplacement>;
70
72
  }
73
+ export interface OrderSerial {
74
+ id: string;
75
+ productId: string;
76
+ status: 'DELIVERED' | 'REPLACED';
77
+ deliveredAt: string | null;
78
+ /** The serial that took this one's place, once replaced. */
79
+ replacedById: string | null;
80
+ }
71
81
  export interface OrderReplacement {
72
82
  id: string;
73
83
  productId: string;
@@ -195,6 +205,13 @@ export interface Product {
195
205
  deliveryType: DeliveryType | null;
196
206
  /** Server stores: what the game server runs after payment. */
197
207
  deliveryPayload: string | null;
208
+ revokePayload: string | null;
209
+ coinConfig: CoinConfig | null;
210
+ pendingChanges: {
211
+ submittedAt: string;
212
+ changes: Partial<Record<ReviewedProductField, unknown>>;
213
+ } | null;
214
+ pendingChangesRejection: string | null;
198
215
  visibility: ProductVisibility;
199
216
  /** Higher comes first on the store page. */
200
217
  sortPriority: number;
@@ -222,8 +239,24 @@ export interface Product {
222
239
  createdAt: string;
223
240
  updatedAt: string;
224
241
  }
242
+ export interface CoinBonusTier {
243
+ thresholdCoins: number;
244
+ bonusPercent: number;
245
+ }
246
+ export interface CoinConfig {
247
+ unitPriceCents: number;
248
+ minQty: number;
249
+ maxQty: number;
250
+ step: number;
251
+ currency: string;
252
+ bonusTiers: Array<CoinBonusTier>;
253
+ }
254
+ export type CoinConfigInput = Omit<CoinConfig, 'currency'>;
255
+ export type ReviewedProductField = 'name' | 'shortDescription' | 'description' | 'images' | 'videoUrl' | 'features' | 'requirements' | 'customFields' | 'deliveryNote' | 'termsOfService' | 'warrantyText' | 'deliveryType' | 'deliveryPayload' | 'revokePayload';
225
256
  export interface ListProductsParams {
226
257
  status?: ProductStatus | 'all';
258
+ limit?: number;
259
+ offset?: number;
227
260
  }
228
261
  interface ProductFields {
229
262
  slug?: string;
@@ -245,6 +278,8 @@ interface ProductFields {
245
278
  deliveryType?: DeliveryType;
246
279
  /** Required for a server store; needs a placeholder such as {steamhex}. */
247
280
  deliveryPayload?: string;
281
+ revokePayload?: string;
282
+ coinConfig?: CoinConfigInput;
248
283
  visibility?: ProductVisibility;
249
284
  sortPriority?: number;
250
285
  /** null or an empty string clears it. */
@@ -269,7 +304,7 @@ export interface CreateProductParams extends ProductFields {
269
304
  name: string;
270
305
  price: number;
271
306
  comparePrice?: number;
272
- productType?: 'ONE_TIME' | 'RECURRING';
307
+ productType?: 'ONE_TIME' | 'RECURRING' | 'COINS';
273
308
  submit?: boolean;
274
309
  }
275
310
  export interface UpdateProductParams extends ProductFields {
@@ -427,7 +462,6 @@ export interface Customer {
427
462
  name: string | null;
428
463
  note: string | null;
429
464
  country: string | null;
430
- balance: number;
431
465
  ordersCount: number;
432
466
  totalSpent: number;
433
467
  currency: Currency;
@@ -436,17 +470,8 @@ export interface Customer {
436
470
  createdAt: string;
437
471
  updatedAt: string;
438
472
  }
439
- export interface CustomerBalanceEntry {
440
- id: string;
441
- amount: number;
442
- balanceAfter: number;
443
- reason: string | null;
444
- orderId: string | null;
445
- createdAt: string;
446
- }
447
473
  export interface CustomerDetails extends Customer {
448
474
  recentOrders: Array<Order>;
449
- balanceHistory: Array<CustomerBalanceEntry>;
450
475
  }
451
476
  export interface ListCustomersParams {
452
477
  search?: string;
@@ -468,10 +493,6 @@ export interface UpdateCustomerParams {
468
493
  name?: string | null;
469
494
  note?: string | null;
470
495
  }
471
- export interface AdjustCustomerBalanceParams {
472
- amount: number;
473
- reason?: string;
474
- }
475
496
  export interface FeedbackReply {
476
497
  id: string;
477
498
  content: string;
@@ -803,6 +824,16 @@ export interface OrderRefundedData {
803
824
  granted?: boolean;
804
825
  fullRefund: boolean;
805
826
  licenses: Array<WebhookLicense>;
827
+ revocations: Array<Revocation>;
828
+ }
829
+ export interface Revocation {
830
+ productId: string;
831
+ productSlug: string;
832
+ productName: string;
833
+ deliveryKey: string;
834
+ delivered: boolean;
835
+ deliveryType: DeliveryType;
836
+ revokeCommand: string;
806
837
  }
807
838
  export interface OrderDisputedData {
808
839
  orderId: string;
@@ -869,19 +900,20 @@ export interface WebhookTestData {
869
900
  webhookId: string;
870
901
  sample: Partial<OrderPaidData>;
871
902
  }
872
- export type OrderEventData = Omit<Order, 'serialsCount' | 'serialsReplaced' | 'replacements'> & {
903
+ export type OrderEventData = Omit<Order, 'serialsCount' | 'serialsReplaced' | 'serials' | 'replacements'> & {
873
904
  previousStatus: OrderStatus | null;
905
+ /** Dedupe key: `order.created:<orderId>`, `order.updated:<orderId>:<transition>` or `order.canceled:<orderId>`. */
906
+ idempotencyKey: string;
874
907
  };
875
- export interface OrderReplacedData extends Omit<Order, 'serialsCount' | 'serialsReplaced' | 'replacements'> {
908
+ export interface OrderReplacedData extends Omit<Order, 'serialsCount' | 'serialsReplaced' | 'serials' | 'replacements'> {
909
+ /** Dedupe key: `order.replaced:<orderId>:<first new serial id>`. */
910
+ idempotencyKey: string;
876
911
  reason: string | null;
877
- replacements: Array<{
878
- productId: string;
879
- oldSerialId: string;
880
- newSerialId: string;
881
- key: string;
882
- }>;
912
+ replacements: Array<OrderKeyReplacement>;
883
913
  }
884
914
  export interface QueryEventData {
915
+ /** Dedupe key: `query.created:<queryId>` or `query.replied:<messageId>`. */
916
+ idempotencyKey: string;
885
917
  queryId: string;
886
918
  title: string;
887
919
  status: QueryStatus;
@@ -895,6 +927,8 @@ export interface QueryEventData {
895
927
  };
896
928
  }
897
929
  export interface FeedbackReceivedData {
930
+ /** Dedupe key: `feedback.received:<feedbackId>`. */
931
+ idempotencyKey: string;
898
932
  feedbackId: string;
899
933
  productId: string;
900
934
  productName: string;
@@ -915,6 +949,7 @@ export type SkalpEvent = Envelope<'order.created' | 'order.updated' | 'order.can
915
949
  }> | Envelope<'subscription.canceled', SubscriptionEventData & {
916
950
  endedAt: string | null;
917
951
  cancelReason: 'requested' | 'payment_failure' | 'dispute' | 'stripe';
952
+ revokeCommand?: string;
918
953
  }> | Envelope<'subscription.refunded', SubscriptionEventData & {
919
954
  invoiceId: string;
920
955
  amountRefunded: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalpgg/sdk",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Official Skalp SDK for Node.js and edge runtimes: checkout, subscriptions, catalog and key stock, webhooks and license verification.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://skalp.gg/docs/sdk",