@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 +7 -10
- package/dist/cjs/index.d.ts +3 -4
- package/dist/cjs/index.js +22 -5
- package/dist/cjs/types.d.ts +58 -23
- package/dist/esm/index.d.ts +3 -4
- package/dist/esm/index.js +22 -5
- package/dist/esm/types.d.ts +58 -23
- package/package.json +1 -1
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? })` / `
|
|
74
|
-
| `
|
|
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 `
|
|
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`)
|
|
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
|
|
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
|
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AccessRule, AddAccessRuleParams, AddSerialsParams, AddSerialsResult,
|
|
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
|
+
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.
|
|
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 } = {}) =>
|
|
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,
|
|
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
|
}
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -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/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AccessRule, AddAccessRuleParams, AddSerialsParams, AddSerialsResult,
|
|
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
|
+
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.
|
|
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 } = {}) =>
|
|
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,
|
|
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
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -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
|
+
"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",
|