@skalpgg/sdk 1.1.0 → 1.3.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
@@ -1,6 +1,6 @@
1
1
  # skalp
2
2
 
3
- The official Skalp SDK for Node.js and edge runtimes: hosted checkout, subscriptions, webhook verification and license checks, with types for every response.
3
+ The official Skalp SDK for Node.js and edge runtimes: hosted checkout, subscriptions, your product catalog and key stock, webhook verification and license checks, with types for every response.
4
4
 
5
5
  Full documentation with Next.js, Express, NestJS, Hono and Fastify examples: https://skalp.gg/docs/sdk
6
6
 
@@ -41,22 +41,121 @@ Create the key on the API keys page of your dashboard. Each key is bound to one
41
41
 
42
42
  | Method | Scope |
43
43
  | --- | --- |
44
- | `checkout.create({ productId, customerEmail?, metadata?, couponCode?, returnUrl? })` | `checkout:write` |
44
+ | `checkout.create({ productId, quantity?, customerEmail?, metadata?, couponCode?, returnUrl?, customFields? })` | `checkout:write` |
45
45
  | `checkout.get(id)` | `checkout:read` |
46
+ | `orders.list({ status?, productId?, email?, createdAfter?, createdBefore?, updatedSince?, limit?, offset? })` | `orders:read` |
46
47
  | `orders.get(id)` | `orders:read` |
47
- | `products.list()` | `products:read` |
48
+ | `orders.cancel(id)` | `orders:write` |
49
+ | `orders.replace(id, { serialIds?, values?, reason? })` | `orders:write` |
50
+ | `products.list({ status? })` | `products:read` |
51
+ | `products.get(idOrSlug)` | `products:read` |
52
+ | `products.create({ name, price, fulfillmentType?, submit?, ... })` | `products:write` |
53
+ | `products.update(idOrSlug, { price?, status?: 'DRAFT' \| 'ARCHIVED', ... })` | `products:write` |
54
+ | `products.archive(idOrSlug)` | `products:write` |
55
+ | `products.submit(idOrSlug)` | `products:write` |
56
+ | `products.stock(idOrSlug)` | `products:read` |
57
+ | `products.listSerials(idOrSlug, { status?, limit?, offset? })` | `products:write` |
58
+ | `products.addSerials(idOrSlug, { serials?, text?, delimiter? })` | `products:write` |
59
+ | `products.deleteSerial(idOrSlug, serialId)` | `products:write` |
60
+ | `categories.list()` | `products:read` |
61
+ | `categories.create({ name })` / `categories.update(id, { name?, sortOrder? })` / `categories.delete(id)` | `products:write` |
62
+ | `groups.list()` / `groups.get(id)` | `products:read` |
63
+ | `groups.create({ title, productIds, ... })` / `groups.update(id, { ... })` / `groups.delete(id)` | `products:write` |
64
+ | `coupons.list()` / `coupons.get(id)` | `coupons:read` |
65
+ | `coupons.create({ code, discountType?, discountPercent?, discountAmount?, validFrom?, validTo?, maxUses?, ... })` / `coupons.update(id, { ... })` / `coupons.delete(id)` | `coupons:write` |
66
+ | `customers.list({ search?, limit?, offset? })` / `customers.get(id)` | `customers:read` |
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
+ | `feedback.list({ productId?, rating?, limit?, offset? })` / `feedback.get(id)` | `feedback:read` |
70
+ | `feedback.reply(id, { message })` / `feedback.appeal(id, { reason })` | `feedback:write` |
71
+ | `queries.list({ status?, limit?, offset? })` / `queries.get(id)` | `queries:read` |
72
+ | `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` |
48
75
  | `subscriptions.createCheckout({ productId \| productSlug, externalRef?, customerEmail?, currency?, ... })` | `checkout:write` |
49
76
  | `subscriptions.getCheckout(id)` | `checkout:read` |
50
77
  | `subscriptions.list({ externalRef?, status?, updatedSince?, limit?, offset? })` | `subscriptions:read` |
51
78
  | `subscriptions.get(id)` | `subscriptions:read` |
52
79
  | `subscriptions.changePlan(id, { productId \| productSlug })` | `subscriptions:write` |
53
80
  | `subscriptions.cancel(id)` | `subscriptions:write` |
81
+ | `subscriptions.createUsageCharge(id, { amount, currency, description, idempotencyKey, periodStart, periodEnd })` | `subscriptions:write` |
54
82
  | `subscriptions.listInvoices(id)` | `subscriptions:read` |
83
+ | `coins.createCheckout({ steamHex, coins, idempotencyKey?, returnUrl? })` | `coins:purchase` |
84
+ | `coins.getCheckoutStatus(reference)` | `coins:read` |
85
+ | `licenses.validate({ key, productId?, hardwareId? })` | `licenses:validate` |
86
+ | `licenses.resetDevices({ key, hardwareId? })` | `licenses:write` |
55
87
  | `licenses.verify({ licenseKey, serverIp, serverKey, serverPort? })` | none |
56
88
  | `licenses.heartbeat({ licenseKey, serverIp })` | none |
57
89
  | `webhooks.constructEvent(rawBody, headers)` | none |
58
90
  | `webhooks.verify(rawBody, headers)` | none |
59
91
 
92
+ `products:write` does not include `products:read`: a key that manages the catalog usually needs both. Every catalog call counts against the key's rate limit.
93
+
94
+ ## Catalog
95
+
96
+ ```ts
97
+ const product = await skalp.products.create({
98
+ name: 'Aurora Pro key',
99
+ price: 19.5,
100
+ fulfillmentType: 'SERIAL',
101
+ })
102
+
103
+ const { added, skipped, stock } = await skalp.products.addSerials(product.id, {
104
+ text: await readFile('keys.txt', 'utf8'),
105
+ })
106
+
107
+ await skalp.products.submit(product.id)
108
+ ```
109
+
110
+ A product starts as a `DRAFT`. `submit` sends it to review, and the review sets `ACTIVE` or `REJECTED`; read the product to see which. `update` accepts only `DRAFT` and `ARCHIVED` as a `status`, and `archive` takes a product off sale without deleting anything. Pass `null` to `update` to clear `comparePrice`, `storeCategoryId`, `dynamicUrl` or `licenseDurationDays`.
111
+
112
+ `addSerials` splits `text` on new lines unless you pass `delimiter` (`'comma'`, `'semicolon'` or any other string). A key the product already has is counted in `skipped` and not added again. `listSerials` returns the plain key values, which is why it needs `products:write`.
113
+
114
+ ## Orders and customers
115
+
116
+ ```ts
117
+ const { data: orders } = await skalp.orders.list({ status: 'COMPLETED', updatedSince: lastSync, limit: 100 })
118
+
119
+ await skalp.orders.cancel(order.id)
120
+
121
+ 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
+ ```
125
+
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.
127
+
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.
129
+
130
+ Coupons take `validFrom`, `validTo` as an ISO string or a `Date`; `coupons.update` accepts `null` to clear `validTo`, `maxUses`, `minPurchaseAmount` and `description`.
131
+
132
+ ## Support and access rules
133
+
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.
135
+
136
+ ```ts
137
+ await skalp.blacklist.add({ type: 'EMAIL', value: 'chargeback@example.com', note: 'Disputed order 1042' })
138
+ ```
139
+
140
+ `type` is `EMAIL`, `IP`, `COUNTRY`, `ISP`, `ASN` or `HOST`. `blacklist.list` and `whitelist.list` return every rule as one array, not a page.
141
+
142
+ ## Usage charges
143
+
144
+ ```ts
145
+ const charge = await skalp.subscriptions.createUsageCharge(subscription.id, {
146
+ amount: 12.34,
147
+ currency: 'EUR',
148
+ description: 'Traffic over your plan, September 2026',
149
+ idempotencyKey: `overage:${tenantId}:2026-09`,
150
+ periodStart: new Date('2026-09-01T00:00:00Z'),
151
+ periodEnd: new Date('2026-10-01T00:00:00Z'),
152
+ })
153
+
154
+ if (charge.status === 'open') await emailPaymentLink(charge.hostedUrl)
155
+ ```
156
+
157
+ The same `idempotencyKey` always returns the same charge, so retry with it after a timeout or a 5xx.
158
+
60
159
  ## Webhooks
61
160
 
62
161
  Pass the raw request body, not parsed JSON, together with the request headers. `constructEvent` checks the `Skalp-Signature` header, an HMAC-SHA256 over the timestamp and the exact bytes Skalp sent, and rejects deliveries older than 5 minutes.
@@ -75,6 +174,10 @@ export async function POST(request: Request) {
75
174
 
76
175
  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`.
77
176
 
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.
178
+
179
+ `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
+
78
181
  ## Errors
79
182
 
80
183
  A refusal from the API throws `SkalpError` with `status`, `code` (for example `STORE_REQUIRED`) and the parsed `body`. A network failure or a timeout throws `SkalpConnectionError`, naming the call and keeping the original in `cause`, so the two are easy to tell apart:
@@ -89,6 +192,6 @@ try {
89
192
  }
90
193
  ```
91
194
 
92
- A `429` is retried up to `maxRetries` times (default 2) when the limit resets within 10 seconds. `constructEvent` throws `SkalpSignatureError` with `reason: 'stale' | 'invalid'` — `'stale'` means the delivery is outside the 5 minute replay window, `'invalid'` means the bytes or the secret do not match.
195
+ A `429` is retried up to `maxRetries` times (default 2) when the limit resets within 10 seconds. `constructEvent` throws `SkalpSignatureError` with `reason: 'stale' | 'invalid'`: `'stale'` means the delivery is outside the 5 minute replay window, `'invalid'` means the bytes or the secret do not match.
93
196
 
94
197
  The constructor throws if it finds an API key in a browser, so the mistake shows up the first time you run the code rather than after a secret key has shipped to users. Pass `dangerouslyAllowBrowser: true` to override it.
@@ -1,6 +1,6 @@
1
- import type { ChangeSubscriptionPlanParams, CheckoutSession, CheckoutSessionStatus, CreateCheckoutParams, CreateSubscriptionCheckoutParams, HeartbeatParams, Invoice, LicenseVerification, ListSubscriptionsParams, Order, Product, SkalpEvent, Subscription, SubscriptionCheckout, SubscriptionCheckoutStatus, SubscriptionList, VerifyLicenseParams } from './types.js';
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';
2
2
  export type * from './types.js';
3
- export declare const VERSION = "1.1.0";
3
+ export declare const VERSION = "1.3.0";
4
4
  export interface SkalpOptions {
5
5
  apiKey?: string;
6
6
  webhookSecret?: string;
@@ -32,14 +32,81 @@ export declare class Skalp {
32
32
  #private;
33
33
  constructor(options?: string | SkalpOptions);
34
34
  readonly checkout: {
35
- create: ({ productId, customerEmail, metadata, couponCode, returnUrl }: CreateCheckoutParams) => Promise<CheckoutSession>;
35
+ create: ({ productId, quantity, customerEmail, metadata, couponCode, returnUrl, customFields }: CreateCheckoutParams) => Promise<CheckoutSession>;
36
36
  get: (id: string) => Promise<CheckoutSessionStatus>;
37
37
  };
38
38
  readonly orders: {
39
+ list: ({ status, productId, email, createdAfter, createdBefore, updatedSince, limit, offset }?: ListOrdersParams) => Promise<OrderList>;
39
40
  get: (id: string) => Promise<Order>;
41
+ cancel: (id: string) => Promise<Order>;
42
+ replace: (id: string, { serialIds, values, reason }?: ReplaceOrderKeysParams) => Promise<OrderKeysReplaced>;
40
43
  };
41
44
  readonly products: {
42
- list: () => Promise<Product[]>;
45
+ list: ({ status }?: ListProductsParams) => Promise<Product[]>;
46
+ get: (idOrSlug: string) => Promise<Product>;
47
+ create: (params: CreateProductParams) => Promise<Product>;
48
+ update: (idOrSlug: string, params: UpdateProductParams) => Promise<Product>;
49
+ archive: (idOrSlug: string) => Promise<Product>;
50
+ submit: (idOrSlug: string) => Promise<Product>;
51
+ stock: (idOrSlug: string) => Promise<ProductStock>;
52
+ listSerials: (idOrSlug: string, { status, limit, offset }?: ListSerialsParams) => Promise<SerialList>;
53
+ addSerials: (idOrSlug: string, { serials, text, delimiter }: AddSerialsParams) => Promise<AddSerialsResult>;
54
+ deleteSerial: (idOrSlug: string, serialId: string) => Promise<Deleted>;
55
+ };
56
+ readonly categories: {
57
+ list: () => Promise<StoreCategoryWithCount[]>;
58
+ create: ({ name }: CreateStoreCategoryParams) => Promise<StoreCategory>;
59
+ update: (id: string, { name, sortOrder }: UpdateStoreCategoryParams) => Promise<StoreCategory>;
60
+ delete: (id: string) => Promise<Deleted>;
61
+ };
62
+ readonly groups: {
63
+ list: () => Promise<ProductGroup[]>;
64
+ get: (id: string) => Promise<ProductGroup>;
65
+ create: (params: CreateProductGroupParams) => Promise<ProductGroup>;
66
+ update: (id: string, params: UpdateProductGroupParams) => Promise<ProductGroup>;
67
+ delete: (id: string) => Promise<Deleted>;
68
+ };
69
+ readonly coupons: {
70
+ list: () => Promise<Coupon[]>;
71
+ get: (id: string) => Promise<Coupon>;
72
+ create: (params: CreateCouponParams) => Promise<Coupon>;
73
+ update: (id: string, params: UpdateCouponParams) => Promise<Coupon>;
74
+ delete: (id: string) => Promise<Deleted>;
75
+ };
76
+ readonly customers: {
77
+ list: ({ search, limit, offset }?: ListCustomersParams) => Promise<CustomerList>;
78
+ get: (id: string) => Promise<CustomerDetails>;
79
+ create: ({ email, name, note }: CreateCustomerParams) => Promise<Customer>;
80
+ update: (id: string, { name, note }: UpdateCustomerParams) => Promise<Customer>;
81
+ delete: (id: string) => Promise<Deleted>;
82
+ adjustBalance: (id: string, { amount, reason }: AdjustCustomerBalanceParams) => Promise<Customer>;
83
+ };
84
+ readonly feedback: {
85
+ list: ({ productId, rating, limit, offset }?: ListFeedbackParams) => Promise<FeedbackList>;
86
+ get: (id: string) => Promise<Feedback>;
87
+ reply: (id: string, { message }: ReplyParams) => Promise<Feedback>;
88
+ appeal: (id: string, { reason }: AppealFeedbackParams) => Promise<Feedback>;
89
+ };
90
+ readonly queries: {
91
+ list: ({ status, limit, offset }?: ListQueriesParams) => Promise<SupportQueryList>;
92
+ get: (id: string) => Promise<SupportQueryDetails>;
93
+ reply: (id: string, { message }: ReplyParams) => Promise<SupportQueryDetails>;
94
+ close: (id: string) => Promise<SupportQueryDetails>;
95
+ reopen: (id: string) => Promise<SupportQueryDetails>;
96
+ };
97
+ readonly blacklist: {
98
+ list: ({ type }?: ListAccessRulesParams) => Promise<AccessRule[]>;
99
+ add: ({ type, value, note }: AddAccessRuleParams) => Promise<AccessRule>;
100
+ remove: (id: string) => Promise<Deleted>;
101
+ };
102
+ readonly whitelist: {
103
+ list: ({ type }?: ListAccessRulesParams) => Promise<AccessRule[]>;
104
+ add: ({ type, value, note }: AddAccessRuleParams) => Promise<AccessRule>;
105
+ remove: (id: string) => Promise<Deleted>;
106
+ };
107
+ readonly coins: {
108
+ createCheckout: ({ steamHex, coins, idempotencyKey, returnUrl }: CreateCoinCheckoutParams) => Promise<CoinCheckout>;
109
+ getCheckoutStatus: (reference: string) => Promise<CoinCheckoutStatus>;
43
110
  };
44
111
  readonly subscriptions: {
45
112
  createCheckout: ({ productId, productSlug, customerEmail, externalRef, metadata, currency, returnUrl, }: CreateSubscriptionCheckoutParams) => Promise<SubscriptionCheckout>;
@@ -48,6 +115,7 @@ export declare class Skalp {
48
115
  get: (id: string) => Promise<Subscription>;
49
116
  changePlan: (id: string, { productId, productSlug }: ChangeSubscriptionPlanParams) => Promise<Subscription>;
50
117
  cancel: (id: string) => Promise<Subscription>;
118
+ createUsageCharge: (id: string, { amount, currency, description, idempotencyKey, periodStart, periodEnd }: CreateUsageChargeParams) => Promise<UsageCharge>;
51
119
  listInvoices: (id: string) => Promise<Invoice[]>;
52
120
  };
53
121
  readonly licenses: {
@@ -55,6 +123,10 @@ export declare class Skalp {
55
123
  heartbeat: ({ licenseKey, serverIp }: HeartbeatParams) => Promise<{
56
124
  valid: boolean;
57
125
  }>;
126
+ validate: ({ key, productId, hardwareId }: ValidateLicenseParams) => Promise<LicenseValidation>;
127
+ resetDevices: ({ key, hardwareId }: ResetLicenseDevicesParams) => Promise<{
128
+ license: LicenseKeyInfo;
129
+ }>;
58
130
  };
59
131
  readonly webhooks: {
60
132
  verify: (payload: WebhookPayload, signature: SignatureSource, secret?: string) => Promise<boolean>;
package/dist/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
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.1.0';
4
+ exports.VERSION = '1.3.0';
5
5
  const SIGNATURE_HEADER = 'skalp-signature';
6
6
  const SIGNATURE_TOLERANCE_SECONDS = 300;
7
7
  const USER_AGENT = `skalp-node/${exports.VERSION}`;
@@ -40,6 +40,49 @@ class SkalpSignatureError extends Error {
40
40
  exports.SkalpSignatureError = SkalpSignatureError;
41
41
  const env = (name) => globalThis.process?.env?.[name];
42
42
  const segment = (id) => encodeURIComponent(id);
43
+ const isoDate = (value) => (value instanceof Date ? value.toISOString() : value);
44
+ const optionalDate = (value) => value instanceof Date ? value.toISOString() : value;
45
+ const PRODUCT_FIELDS = [
46
+ 'name',
47
+ 'slug',
48
+ 'description',
49
+ 'shortDescription',
50
+ 'price',
51
+ 'comparePrice',
52
+ 'category',
53
+ 'categoryId',
54
+ 'storeCategoryId',
55
+ 'tags',
56
+ 'images',
57
+ 'videoUrl',
58
+ 'fulfillmentType',
59
+ 'billingInterval',
60
+ 'trialPeriodDays',
61
+ 'dynamicUrl',
62
+ 'licenseDeviceLimit',
63
+ 'licenseDurationDays',
64
+ ];
65
+ const COUPON_FIELDS = [
66
+ 'discountType',
67
+ 'discountPercent',
68
+ 'discountAmount',
69
+ 'productIds',
70
+ 'disabledWithVolumeDiscounts',
71
+ 'description',
72
+ 'maxUses',
73
+ 'minPurchaseAmount',
74
+ 'oneTimePerUser',
75
+ ];
76
+ const GROUP_FIELDS = ['title', 'productIds', 'description', 'image', 'sortPriority', 'unlisted'];
77
+ function pick(source, keys) {
78
+ const picked = {};
79
+ for (const key of keys) {
80
+ const value = source[key];
81
+ if (value !== undefined)
82
+ picked[key] = value;
83
+ }
84
+ return picked;
85
+ }
43
86
  class Skalp {
44
87
  #apiKey;
45
88
  #webhookSecret;
@@ -60,16 +103,117 @@ class Skalp {
60
103
  }
61
104
  }
62
105
  checkout = {
63
- create: ({ productId, customerEmail, metadata, couponCode, returnUrl }) => this.#request('POST', '/v1/checkout/sessions', {
64
- body: { items: [{ productId, quantity: 1 }], customerEmail, metadata, couponCode, returnUrl },
106
+ create: ({ productId, quantity, customerEmail, metadata, couponCode, returnUrl, customFields }) => this.#request('POST', '/v1/checkout/sessions', {
107
+ body: { items: [{ productId, quantity }], customerEmail, metadata, couponCode, returnUrl, customFields },
65
108
  }),
66
109
  get: (id) => this.#request('GET', `/v1/checkout/sessions/${segment(id)}`),
67
110
  };
68
111
  orders = {
112
+ list: ({ status, productId, email, createdAfter, createdBefore, updatedSince, limit, offset } = {}) => this.#request('GET', '/v1/orders', {
113
+ query: {
114
+ status,
115
+ productId,
116
+ email,
117
+ createdAfter: optionalDate(createdAfter),
118
+ createdBefore: optionalDate(createdBefore),
119
+ updatedSince: optionalDate(updatedSince),
120
+ limit,
121
+ offset,
122
+ },
123
+ }),
69
124
  get: (id) => this.#request('GET', `/v1/orders/${segment(id)}`),
125
+ cancel: (id) => this.#request('POST', `/v1/orders/${segment(id)}/cancel`),
126
+ replace: (id, { serialIds, values, reason } = {}) => this.#request('POST', `/v1/orders/${segment(id)}/replacement`, {
127
+ body: { serialIds, values, reason },
128
+ }),
70
129
  };
71
130
  products = {
72
- list: () => this.#request('GET', '/v1/products'),
131
+ list: ({ status } = {}) => this.#request('GET', '/v1/products', { query: { status } }),
132
+ get: (idOrSlug) => this.#request('GET', `/v1/products/${segment(idOrSlug)}`),
133
+ create: (params) => this.#request('POST', '/v1/products', {
134
+ body: pick(params, [...PRODUCT_FIELDS, 'productType', 'submit']),
135
+ }),
136
+ update: (idOrSlug, params) => this.#request('PATCH', `/v1/products/${segment(idOrSlug)}`, {
137
+ body: pick(params, [...PRODUCT_FIELDS, 'status']),
138
+ }),
139
+ archive: (idOrSlug) => this.#request('DELETE', `/v1/products/${segment(idOrSlug)}`),
140
+ submit: (idOrSlug) => this.#request('POST', `/v1/products/${segment(idOrSlug)}/submit`),
141
+ stock: (idOrSlug) => this.#request('GET', `/v1/products/${segment(idOrSlug)}/stock`),
142
+ listSerials: (idOrSlug, { status, limit, offset } = {}) => this.#request('GET', `/v1/products/${segment(idOrSlug)}/serials`, {
143
+ query: { status, limit, offset },
144
+ }),
145
+ addSerials: (idOrSlug, { serials, text, delimiter }) => this.#request('POST', `/v1/products/${segment(idOrSlug)}/serials`, {
146
+ body: { serials, text, delimiter },
147
+ }),
148
+ deleteSerial: (idOrSlug, serialId) => this.#request('DELETE', `/v1/products/${segment(idOrSlug)}/serials/${segment(serialId)}`),
149
+ };
150
+ categories = {
151
+ list: () => this.#request('GET', '/v1/categories'),
152
+ create: ({ name }) => this.#request('POST', '/v1/categories', { body: { name } }),
153
+ update: (id, { name, sortOrder }) => this.#request('PATCH', `/v1/categories/${segment(id)}`, { body: { name, sortOrder } }),
154
+ delete: (id) => this.#request('DELETE', `/v1/categories/${segment(id)}`),
155
+ };
156
+ groups = {
157
+ list: () => this.#request('GET', '/v1/groups'),
158
+ get: (id) => this.#request('GET', `/v1/groups/${segment(id)}`),
159
+ create: (params) => this.#request('POST', '/v1/groups', { body: pick(params, GROUP_FIELDS) }),
160
+ update: (id, params) => this.#request('PATCH', `/v1/groups/${segment(id)}`, { body: pick(params, GROUP_FIELDS) }),
161
+ delete: (id) => this.#request('DELETE', `/v1/groups/${segment(id)}`),
162
+ };
163
+ coupons = {
164
+ list: () => this.#request('GET', '/v1/coupons'),
165
+ get: (id) => this.#request('GET', `/v1/coupons/${segment(id)}`),
166
+ create: (params) => this.#request('POST', '/v1/coupons', {
167
+ body: {
168
+ ...pick(params, [...COUPON_FIELDS, 'code']),
169
+ validFrom: optionalDate(params.validFrom),
170
+ validTo: optionalDate(params.validTo),
171
+ },
172
+ }),
173
+ update: (id, params) => this.#request('PATCH', `/v1/coupons/${segment(id)}`, {
174
+ body: {
175
+ ...pick(params, [...COUPON_FIELDS, 'isActive']),
176
+ validFrom: optionalDate(params.validFrom),
177
+ validTo: optionalDate(params.validTo),
178
+ },
179
+ }),
180
+ delete: (id) => this.#request('DELETE', `/v1/coupons/${segment(id)}`),
181
+ };
182
+ customers = {
183
+ list: ({ search, limit, offset } = {}) => this.#request('GET', '/v1/customers', { query: { search, limit, offset } }),
184
+ get: (id) => this.#request('GET', `/v1/customers/${segment(id)}`),
185
+ create: ({ email, name, note }) => this.#request('POST', '/v1/customers', { body: { email, name, note } }),
186
+ update: (id, { name, note }) => this.#request('PATCH', `/v1/customers/${segment(id)}`, { body: { name, note } }),
187
+ 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
+ };
190
+ feedback = {
191
+ list: ({ productId, rating, limit, offset } = {}) => this.#request('GET', '/v1/feedback', { query: { productId, rating, limit, offset } }),
192
+ get: (id) => this.#request('GET', `/v1/feedback/${segment(id)}`),
193
+ reply: (id, { message }) => this.#request('POST', `/v1/feedback/${segment(id)}/reply`, { body: { message } }),
194
+ appeal: (id, { reason }) => this.#request('POST', `/v1/feedback/${segment(id)}/appeal`, { body: { reason } }),
195
+ };
196
+ queries = {
197
+ list: ({ status, limit, offset } = {}) => this.#request('GET', '/v1/queries', { query: { status, limit, offset } }),
198
+ get: (id) => this.#request('GET', `/v1/queries/${segment(id)}`),
199
+ reply: (id, { message }) => this.#request('POST', `/v1/queries/${segment(id)}/reply`, { body: { message } }),
200
+ close: (id) => this.#request('POST', `/v1/queries/${segment(id)}/close`),
201
+ reopen: (id) => this.#request('POST', `/v1/queries/${segment(id)}/reopen`),
202
+ };
203
+ blacklist = this.#accessRules('blacklist');
204
+ whitelist = this.#accessRules('whitelist');
205
+ #accessRules(list) {
206
+ return {
207
+ list: ({ type } = {}) => this.#request('GET', `/v1/${list}`, { query: { type } }),
208
+ add: ({ type, value, note }) => this.#request('POST', `/v1/${list}`, { body: { type, value, note } }),
209
+ remove: (id) => this.#request('DELETE', `/v1/${list}/${segment(id)}`),
210
+ };
211
+ }
212
+ coins = {
213
+ createCheckout: ({ steamHex, coins, idempotencyKey, returnUrl }) => this.#request('POST', '/coins/checkout', {
214
+ body: { steamHex, coins, idempotencyKey, returnUrl },
215
+ }),
216
+ getCheckoutStatus: (reference) => this.#request('GET', `/coins/checkout/${segment(reference)}/status`),
73
217
  };
74
218
  subscriptions = {
75
219
  createCheckout: ({ productId, productSlug, customerEmail, externalRef, metadata, currency, returnUrl, }) => this.#request('POST', '/v1/subscriptions/checkout', {
@@ -90,6 +234,16 @@ class Skalp {
90
234
  body: { productId, productSlug },
91
235
  }),
92
236
  cancel: (id) => this.#request('DELETE', `/v1/subscriptions/${segment(id)}`),
237
+ createUsageCharge: (id, { amount, currency, description, idempotencyKey, periodStart, periodEnd }) => this.#request('POST', `/v1/subscriptions/${segment(id)}/usage-charges`, {
238
+ body: {
239
+ amount,
240
+ currency,
241
+ description,
242
+ idempotencyKey,
243
+ periodStart: isoDate(periodStart),
244
+ periodEnd: isoDate(periodEnd),
245
+ },
246
+ }),
93
247
  listInvoices: (id) => this.#request('GET', `/v1/subscriptions/${segment(id)}/invoices`).then((r) => r.data),
94
248
  };
95
249
  licenses = {
@@ -101,6 +255,16 @@ class Skalp {
101
255
  body: { licenseKey, serverIp },
102
256
  auth: false,
103
257
  }),
258
+ // Keys sold with the "License key" delivery. Needs the licenses:validate
259
+ // scope; a key with only that scope may ship inside your app.
260
+ validate: ({ key, productId, hardwareId }) => this.#request('POST', '/v1/licenses/validate', {
261
+ body: { key, productId, hardwareId },
262
+ }),
263
+ // Frees the machines a key is bound to. Needs licenses:write; call it
264
+ // from your server only.
265
+ resetDevices: ({ key, hardwareId }) => this.#request('POST', '/v1/licenses/devices/reset', {
266
+ body: { key, hardwareId },
267
+ }),
104
268
  };
105
269
  webhooks = {
106
270
  verify: async (payload, signature, secret) => (await this.#checkSignature(payload, signature, secret)) === undefined,