@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 +107 -4
- package/dist/cjs/index.d.ts +76 -4
- package/dist/cjs/index.js +168 -4
- package/dist/cjs/types.d.ts +678 -9
- package/dist/esm/index.d.ts +76 -4
- package/dist/esm/index.js +168 -4
- package/dist/esm/types.d.ts +678 -9
- package/package.json +2 -2
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
|
-
| `
|
|
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'
|
|
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.
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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,
|