@skalpgg/sdk 1.0.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 +120 -4
- package/dist/cjs/index.d.ts +87 -8
- package/dist/cjs/index.js +244 -45
- package/dist/cjs/types.d.ts +689 -12
- package/dist/esm/index.d.ts +87 -8
- package/dist/esm/index.js +242 -44
- package/dist/esm/types.d.ts +689 -12
- package/package.json +8 -3
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { 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.0";
|
|
3
4
|
export interface SkalpOptions {
|
|
4
5
|
apiKey?: string;
|
|
5
6
|
webhookSecret?: string;
|
|
@@ -19,35 +20,113 @@ export declare class SkalpError extends Error {
|
|
|
19
20
|
readonly body: unknown;
|
|
20
21
|
constructor(message: string, status: number, code?: string, body?: unknown);
|
|
21
22
|
}
|
|
23
|
+
export declare class SkalpConnectionError extends Error {
|
|
24
|
+
constructor(method: string, path: string, cause: unknown);
|
|
25
|
+
}
|
|
26
|
+
export type SignatureFailure = 'stale' | 'invalid';
|
|
22
27
|
export declare class SkalpSignatureError extends Error {
|
|
23
|
-
|
|
28
|
+
readonly reason: SignatureFailure;
|
|
29
|
+
constructor(reason?: SignatureFailure);
|
|
24
30
|
}
|
|
25
31
|
export declare class Skalp {
|
|
26
32
|
#private;
|
|
27
33
|
constructor(options?: string | SkalpOptions);
|
|
28
34
|
readonly checkout: {
|
|
29
|
-
create: (
|
|
35
|
+
create: ({ productId, quantity, customerEmail, metadata, couponCode, returnUrl, customFields }: CreateCheckoutParams) => Promise<CheckoutSession>;
|
|
30
36
|
get: (id: string) => Promise<CheckoutSessionStatus>;
|
|
31
37
|
};
|
|
32
38
|
readonly orders: {
|
|
39
|
+
list: ({ status, productId, email, createdAfter, createdBefore, updatedSince, limit, offset }?: ListOrdersParams) => Promise<OrderList>;
|
|
33
40
|
get: (id: string) => Promise<Order>;
|
|
41
|
+
cancel: (id: string) => Promise<Order>;
|
|
42
|
+
replace: (id: string, { serialIds, values, reason }?: ReplaceOrderKeysParams) => Promise<OrderKeysReplaced>;
|
|
34
43
|
};
|
|
35
44
|
readonly products: {
|
|
36
|
-
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>;
|
|
37
110
|
};
|
|
38
111
|
readonly subscriptions: {
|
|
39
|
-
createCheckout: (
|
|
112
|
+
createCheckout: ({ productId, productSlug, customerEmail, externalRef, metadata, currency, returnUrl, }: CreateSubscriptionCheckoutParams) => Promise<SubscriptionCheckout>;
|
|
40
113
|
getCheckout: (id: string) => Promise<SubscriptionCheckoutStatus>;
|
|
41
|
-
list: (
|
|
114
|
+
list: ({ externalRef, status, updatedSince, limit, offset }?: ListSubscriptionsParams) => Promise<SubscriptionList>;
|
|
42
115
|
get: (id: string) => Promise<Subscription>;
|
|
116
|
+
changePlan: (id: string, { productId, productSlug }: ChangeSubscriptionPlanParams) => Promise<Subscription>;
|
|
43
117
|
cancel: (id: string) => Promise<Subscription>;
|
|
118
|
+
createUsageCharge: (id: string, { amount, currency, description, idempotencyKey, periodStart, periodEnd }: CreateUsageChargeParams) => Promise<UsageCharge>;
|
|
44
119
|
listInvoices: (id: string) => Promise<Invoice[]>;
|
|
45
120
|
};
|
|
46
121
|
readonly licenses: {
|
|
47
|
-
verify: (
|
|
48
|
-
heartbeat: (
|
|
122
|
+
verify: ({ licenseKey, serverIp, serverKey, serverPort }: VerifyLicenseParams) => Promise<LicenseVerification>;
|
|
123
|
+
heartbeat: ({ licenseKey, serverIp }: HeartbeatParams) => Promise<{
|
|
49
124
|
valid: boolean;
|
|
50
125
|
}>;
|
|
126
|
+
validate: ({ key, productId, hardwareId }: ValidateLicenseParams) => Promise<LicenseValidation>;
|
|
127
|
+
resetDevices: ({ key, hardwareId }: ResetLicenseDevicesParams) => Promise<{
|
|
128
|
+
license: LicenseKeyInfo;
|
|
129
|
+
}>;
|
|
51
130
|
};
|
|
52
131
|
readonly webhooks: {
|
|
53
132
|
verify: (payload: WebhookPayload, signature: SignatureSource, secret?: string) => Promise<boolean>;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export const VERSION = '1.3.0';
|
|
1
2
|
const SIGNATURE_HEADER = 'skalp-signature';
|
|
2
3
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
4
|
+
const USER_AGENT = `skalp-node/${VERSION}`;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
|
+
const MAX_RETRY_WAIT_SECONDS = 10;
|
|
3
7
|
export class SkalpError extends Error {
|
|
4
8
|
status;
|
|
5
9
|
code;
|
|
@@ -12,15 +16,67 @@ export class SkalpError extends Error {
|
|
|
12
16
|
this.body = body;
|
|
13
17
|
}
|
|
14
18
|
}
|
|
19
|
+
export class SkalpConnectionError extends Error {
|
|
20
|
+
constructor(method, path, cause) {
|
|
21
|
+
super(`Skalp: ${method} ${path}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
22
|
+
this.name = 'SkalpConnectionError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
15
25
|
export class SkalpSignatureError extends Error {
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
reason;
|
|
27
|
+
constructor(reason = 'invalid') {
|
|
28
|
+
super(reason === 'stale'
|
|
29
|
+
? 'Skalp: webhook signature is outside the replay window'
|
|
30
|
+
: 'Skalp: invalid webhook signature');
|
|
18
31
|
this.name = 'SkalpSignatureError';
|
|
32
|
+
this.reason = reason;
|
|
19
33
|
}
|
|
20
34
|
}
|
|
21
35
|
const env = (name) => globalThis.process?.env?.[name];
|
|
22
36
|
const segment = (id) => encodeURIComponent(id);
|
|
23
|
-
const
|
|
37
|
+
const isoDate = (value) => (value instanceof Date ? value.toISOString() : value);
|
|
38
|
+
const optionalDate = (value) => value instanceof Date ? value.toISOString() : value;
|
|
39
|
+
const PRODUCT_FIELDS = [
|
|
40
|
+
'name',
|
|
41
|
+
'slug',
|
|
42
|
+
'description',
|
|
43
|
+
'shortDescription',
|
|
44
|
+
'price',
|
|
45
|
+
'comparePrice',
|
|
46
|
+
'category',
|
|
47
|
+
'categoryId',
|
|
48
|
+
'storeCategoryId',
|
|
49
|
+
'tags',
|
|
50
|
+
'images',
|
|
51
|
+
'videoUrl',
|
|
52
|
+
'fulfillmentType',
|
|
53
|
+
'billingInterval',
|
|
54
|
+
'trialPeriodDays',
|
|
55
|
+
'dynamicUrl',
|
|
56
|
+
'licenseDeviceLimit',
|
|
57
|
+
'licenseDurationDays',
|
|
58
|
+
];
|
|
59
|
+
const COUPON_FIELDS = [
|
|
60
|
+
'discountType',
|
|
61
|
+
'discountPercent',
|
|
62
|
+
'discountAmount',
|
|
63
|
+
'productIds',
|
|
64
|
+
'disabledWithVolumeDiscounts',
|
|
65
|
+
'description',
|
|
66
|
+
'maxUses',
|
|
67
|
+
'minPurchaseAmount',
|
|
68
|
+
'oneTimePerUser',
|
|
69
|
+
];
|
|
70
|
+
const GROUP_FIELDS = ['title', 'productIds', 'description', 'image', 'sortPriority', 'unlisted'];
|
|
71
|
+
function pick(source, keys) {
|
|
72
|
+
const picked = {};
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
const value = source[key];
|
|
75
|
+
if (value !== undefined)
|
|
76
|
+
picked[key] = value;
|
|
77
|
+
}
|
|
78
|
+
return picked;
|
|
79
|
+
}
|
|
24
80
|
export class Skalp {
|
|
25
81
|
#apiKey;
|
|
26
82
|
#webhookSecret;
|
|
@@ -33,7 +89,7 @@ export class Skalp {
|
|
|
33
89
|
this.#apiKey = o.apiKey ?? env('SKALP_API_KEY');
|
|
34
90
|
this.#webhookSecret = o.webhookSecret ?? env('SKALP_WEBHOOK_SECRET');
|
|
35
91
|
this.#baseUrl = (o.baseUrl ?? env('SKALP_API_URL') ?? 'https://api.skalp.gg').replace(/\/+$/, '');
|
|
36
|
-
this.#timeout = o.timeout
|
|
92
|
+
this.#timeout = o.timeout && o.timeout > 0 ? o.timeout : DEFAULT_TIMEOUT_MS;
|
|
37
93
|
this.#maxRetries = o.maxRetries ?? 2;
|
|
38
94
|
this.#fetch = o.fetch ?? ((...args) => fetch(...args));
|
|
39
95
|
if (this.#apiKey && 'window' in globalThis && 'document' in globalThis && !o.dangerouslyAllowBrowser) {
|
|
@@ -41,62 +97,198 @@ export class Skalp {
|
|
|
41
97
|
}
|
|
42
98
|
}
|
|
43
99
|
checkout = {
|
|
44
|
-
create: (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
body: { items: [{ productId, quantity: 1 }], ...rest },
|
|
48
|
-
});
|
|
49
|
-
},
|
|
100
|
+
create: ({ productId, quantity, customerEmail, metadata, couponCode, returnUrl, customFields }) => this.#request('POST', '/v1/checkout/sessions', {
|
|
101
|
+
body: { items: [{ productId, quantity }], customerEmail, metadata, couponCode, returnUrl, customFields },
|
|
102
|
+
}),
|
|
50
103
|
get: (id) => this.#request('GET', `/v1/checkout/sessions/${segment(id)}`),
|
|
51
104
|
};
|
|
52
105
|
orders = {
|
|
106
|
+
list: ({ status, productId, email, createdAfter, createdBefore, updatedSince, limit, offset } = {}) => this.#request('GET', '/v1/orders', {
|
|
107
|
+
query: {
|
|
108
|
+
status,
|
|
109
|
+
productId,
|
|
110
|
+
email,
|
|
111
|
+
createdAfter: optionalDate(createdAfter),
|
|
112
|
+
createdBefore: optionalDate(createdBefore),
|
|
113
|
+
updatedSince: optionalDate(updatedSince),
|
|
114
|
+
limit,
|
|
115
|
+
offset,
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
53
118
|
get: (id) => this.#request('GET', `/v1/orders/${segment(id)}`),
|
|
119
|
+
cancel: (id) => this.#request('POST', `/v1/orders/${segment(id)}/cancel`),
|
|
120
|
+
replace: (id, { serialIds, values, reason } = {}) => this.#request('POST', `/v1/orders/${segment(id)}/replacement`, {
|
|
121
|
+
body: { serialIds, values, reason },
|
|
122
|
+
}),
|
|
54
123
|
};
|
|
55
124
|
products = {
|
|
56
|
-
list: () => this.#request('GET', '/v1/products'),
|
|
125
|
+
list: ({ status } = {}) => this.#request('GET', '/v1/products', { query: { status } }),
|
|
126
|
+
get: (idOrSlug) => this.#request('GET', `/v1/products/${segment(idOrSlug)}`),
|
|
127
|
+
create: (params) => this.#request('POST', '/v1/products', {
|
|
128
|
+
body: pick(params, [...PRODUCT_FIELDS, 'productType', 'submit']),
|
|
129
|
+
}),
|
|
130
|
+
update: (idOrSlug, params) => this.#request('PATCH', `/v1/products/${segment(idOrSlug)}`, {
|
|
131
|
+
body: pick(params, [...PRODUCT_FIELDS, 'status']),
|
|
132
|
+
}),
|
|
133
|
+
archive: (idOrSlug) => this.#request('DELETE', `/v1/products/${segment(idOrSlug)}`),
|
|
134
|
+
submit: (idOrSlug) => this.#request('POST', `/v1/products/${segment(idOrSlug)}/submit`),
|
|
135
|
+
stock: (idOrSlug) => this.#request('GET', `/v1/products/${segment(idOrSlug)}/stock`),
|
|
136
|
+
listSerials: (idOrSlug, { status, limit, offset } = {}) => this.#request('GET', `/v1/products/${segment(idOrSlug)}/serials`, {
|
|
137
|
+
query: { status, limit, offset },
|
|
138
|
+
}),
|
|
139
|
+
addSerials: (idOrSlug, { serials, text, delimiter }) => this.#request('POST', `/v1/products/${segment(idOrSlug)}/serials`, {
|
|
140
|
+
body: { serials, text, delimiter },
|
|
141
|
+
}),
|
|
142
|
+
deleteSerial: (idOrSlug, serialId) => this.#request('DELETE', `/v1/products/${segment(idOrSlug)}/serials/${segment(serialId)}`),
|
|
143
|
+
};
|
|
144
|
+
categories = {
|
|
145
|
+
list: () => this.#request('GET', '/v1/categories'),
|
|
146
|
+
create: ({ name }) => this.#request('POST', '/v1/categories', { body: { name } }),
|
|
147
|
+
update: (id, { name, sortOrder }) => this.#request('PATCH', `/v1/categories/${segment(id)}`, { body: { name, sortOrder } }),
|
|
148
|
+
delete: (id) => this.#request('DELETE', `/v1/categories/${segment(id)}`),
|
|
149
|
+
};
|
|
150
|
+
groups = {
|
|
151
|
+
list: () => this.#request('GET', '/v1/groups'),
|
|
152
|
+
get: (id) => this.#request('GET', `/v1/groups/${segment(id)}`),
|
|
153
|
+
create: (params) => this.#request('POST', '/v1/groups', { body: pick(params, GROUP_FIELDS) }),
|
|
154
|
+
update: (id, params) => this.#request('PATCH', `/v1/groups/${segment(id)}`, { body: pick(params, GROUP_FIELDS) }),
|
|
155
|
+
delete: (id) => this.#request('DELETE', `/v1/groups/${segment(id)}`),
|
|
156
|
+
};
|
|
157
|
+
coupons = {
|
|
158
|
+
list: () => this.#request('GET', '/v1/coupons'),
|
|
159
|
+
get: (id) => this.#request('GET', `/v1/coupons/${segment(id)}`),
|
|
160
|
+
create: (params) => this.#request('POST', '/v1/coupons', {
|
|
161
|
+
body: {
|
|
162
|
+
...pick(params, [...COUPON_FIELDS, 'code']),
|
|
163
|
+
validFrom: optionalDate(params.validFrom),
|
|
164
|
+
validTo: optionalDate(params.validTo),
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
update: (id, params) => this.#request('PATCH', `/v1/coupons/${segment(id)}`, {
|
|
168
|
+
body: {
|
|
169
|
+
...pick(params, [...COUPON_FIELDS, 'isActive']),
|
|
170
|
+
validFrom: optionalDate(params.validFrom),
|
|
171
|
+
validTo: optionalDate(params.validTo),
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
delete: (id) => this.#request('DELETE', `/v1/coupons/${segment(id)}`),
|
|
175
|
+
};
|
|
176
|
+
customers = {
|
|
177
|
+
list: ({ search, limit, offset } = {}) => this.#request('GET', '/v1/customers', { query: { search, limit, offset } }),
|
|
178
|
+
get: (id) => this.#request('GET', `/v1/customers/${segment(id)}`),
|
|
179
|
+
create: ({ email, name, note }) => this.#request('POST', '/v1/customers', { body: { email, name, note } }),
|
|
180
|
+
update: (id, { name, note }) => this.#request('PATCH', `/v1/customers/${segment(id)}`, { body: { name, note } }),
|
|
181
|
+
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
|
+
};
|
|
184
|
+
feedback = {
|
|
185
|
+
list: ({ productId, rating, limit, offset } = {}) => this.#request('GET', '/v1/feedback', { query: { productId, rating, limit, offset } }),
|
|
186
|
+
get: (id) => this.#request('GET', `/v1/feedback/${segment(id)}`),
|
|
187
|
+
reply: (id, { message }) => this.#request('POST', `/v1/feedback/${segment(id)}/reply`, { body: { message } }),
|
|
188
|
+
appeal: (id, { reason }) => this.#request('POST', `/v1/feedback/${segment(id)}/appeal`, { body: { reason } }),
|
|
189
|
+
};
|
|
190
|
+
queries = {
|
|
191
|
+
list: ({ status, limit, offset } = {}) => this.#request('GET', '/v1/queries', { query: { status, limit, offset } }),
|
|
192
|
+
get: (id) => this.#request('GET', `/v1/queries/${segment(id)}`),
|
|
193
|
+
reply: (id, { message }) => this.#request('POST', `/v1/queries/${segment(id)}/reply`, { body: { message } }),
|
|
194
|
+
close: (id) => this.#request('POST', `/v1/queries/${segment(id)}/close`),
|
|
195
|
+
reopen: (id) => this.#request('POST', `/v1/queries/${segment(id)}/reopen`),
|
|
196
|
+
};
|
|
197
|
+
blacklist = this.#accessRules('blacklist');
|
|
198
|
+
whitelist = this.#accessRules('whitelist');
|
|
199
|
+
#accessRules(list) {
|
|
200
|
+
return {
|
|
201
|
+
list: ({ type } = {}) => this.#request('GET', `/v1/${list}`, { query: { type } }),
|
|
202
|
+
add: ({ type, value, note }) => this.#request('POST', `/v1/${list}`, { body: { type, value, note } }),
|
|
203
|
+
remove: (id) => this.#request('DELETE', `/v1/${list}/${segment(id)}`),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
coins = {
|
|
207
|
+
createCheckout: ({ steamHex, coins, idempotencyKey, returnUrl }) => this.#request('POST', '/coins/checkout', {
|
|
208
|
+
body: { steamHex, coins, idempotencyKey, returnUrl },
|
|
209
|
+
}),
|
|
210
|
+
getCheckoutStatus: (reference) => this.#request('GET', `/coins/checkout/${segment(reference)}/status`),
|
|
57
211
|
};
|
|
58
212
|
subscriptions = {
|
|
59
|
-
createCheckout: (
|
|
213
|
+
createCheckout: ({ productId, productSlug, customerEmail, externalRef, metadata, currency, returnUrl, }) => this.#request('POST', '/v1/subscriptions/checkout', {
|
|
214
|
+
body: { productId, productSlug, customerEmail, externalRef, metadata, currency, returnUrl },
|
|
215
|
+
}),
|
|
60
216
|
getCheckout: (id) => this.#request('GET', `/v1/subscriptions/checkout/${segment(id)}`),
|
|
61
|
-
list: (
|
|
217
|
+
list: ({ externalRef, status, updatedSince, limit, offset } = {}) => this.#request('GET', '/v1/subscriptions', {
|
|
62
218
|
query: {
|
|
63
|
-
|
|
64
|
-
|
|
219
|
+
externalRef,
|
|
220
|
+
status,
|
|
221
|
+
updatedSince: updatedSince instanceof Date ? updatedSince.toISOString() : updatedSince,
|
|
222
|
+
limit,
|
|
223
|
+
offset,
|
|
65
224
|
},
|
|
66
225
|
}),
|
|
67
226
|
get: (id) => this.#request('GET', `/v1/subscriptions/${segment(id)}`),
|
|
227
|
+
changePlan: (id, { productId, productSlug }) => this.#request('POST', `/v1/subscriptions/${segment(id)}/plan`, {
|
|
228
|
+
body: { productId, productSlug },
|
|
229
|
+
}),
|
|
68
230
|
cancel: (id) => this.#request('DELETE', `/v1/subscriptions/${segment(id)}`),
|
|
231
|
+
createUsageCharge: (id, { amount, currency, description, idempotencyKey, periodStart, periodEnd }) => this.#request('POST', `/v1/subscriptions/${segment(id)}/usage-charges`, {
|
|
232
|
+
body: {
|
|
233
|
+
amount,
|
|
234
|
+
currency,
|
|
235
|
+
description,
|
|
236
|
+
idempotencyKey,
|
|
237
|
+
periodStart: isoDate(periodStart),
|
|
238
|
+
periodEnd: isoDate(periodEnd),
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
69
241
|
listInvoices: (id) => this.#request('GET', `/v1/subscriptions/${segment(id)}/invoices`).then((r) => r.data),
|
|
70
242
|
};
|
|
71
243
|
licenses = {
|
|
72
|
-
verify: (
|
|
73
|
-
|
|
244
|
+
verify: ({ licenseKey, serverIp, serverKey, serverPort }) => this.#request('POST', '/licenses/verify', {
|
|
245
|
+
body: { licenseKey, serverIp, serverKey, serverPort },
|
|
246
|
+
auth: false,
|
|
247
|
+
}),
|
|
248
|
+
heartbeat: ({ licenseKey, serverIp }) => this.#request('POST', '/licenses/heartbeat', {
|
|
249
|
+
body: { licenseKey, serverIp },
|
|
250
|
+
auth: false,
|
|
251
|
+
}),
|
|
252
|
+
// Keys sold with the "License key" delivery. Needs the licenses:validate
|
|
253
|
+
// scope; a key with only that scope may ship inside your app.
|
|
254
|
+
validate: ({ key, productId, hardwareId }) => this.#request('POST', '/v1/licenses/validate', {
|
|
255
|
+
body: { key, productId, hardwareId },
|
|
256
|
+
}),
|
|
257
|
+
// Frees the machines a key is bound to. Needs licenses:write; call it
|
|
258
|
+
// from your server only.
|
|
259
|
+
resetDevices: ({ key, hardwareId }) => this.#request('POST', '/v1/licenses/devices/reset', {
|
|
260
|
+
body: { key, hardwareId },
|
|
261
|
+
}),
|
|
74
262
|
};
|
|
75
263
|
webhooks = {
|
|
76
|
-
verify: async (payload, signature, secret) =>
|
|
77
|
-
const key = secret ?? this.#webhookSecret;
|
|
78
|
-
if (!key) {
|
|
79
|
-
throw new Error('Skalp: missing webhook secret. Pass it as the third argument or set SKALP_WEBHOOK_SECRET.');
|
|
80
|
-
}
|
|
81
|
-
const encoder = new TextEncoder();
|
|
82
|
-
const body = toBytes(payload, encoder);
|
|
83
|
-
const parsed = parseSignature(readSignature(signature));
|
|
84
|
-
if (!parsed || Math.abs(Date.now() / 1000 - parsed.timestamp) > SIGNATURE_TOLERANCE_SECONDS)
|
|
85
|
-
return false;
|
|
86
|
-
const prefix = encoder.encode(`${parsed.timestamp}.`);
|
|
87
|
-
const signed = new Uint8Array(prefix.length + body.length);
|
|
88
|
-
signed.set(prefix);
|
|
89
|
-
signed.set(body, prefix.length);
|
|
90
|
-
const hmac = await crypto.subtle.importKey('raw', encoder.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
91
|
-
return crypto.subtle.verify('HMAC', hmac, parsed.signature, signed);
|
|
92
|
-
},
|
|
264
|
+
verify: async (payload, signature, secret) => (await this.#checkSignature(payload, signature, secret)) === undefined,
|
|
93
265
|
constructEvent: async (payload, signature, secret) => {
|
|
94
|
-
|
|
95
|
-
|
|
266
|
+
const failure = await this.#checkSignature(payload, signature, secret);
|
|
267
|
+
if (failure)
|
|
268
|
+
throw new SkalpSignatureError(failure);
|
|
96
269
|
const text = typeof payload === 'string' ? payload : new TextDecoder().decode(payload);
|
|
97
270
|
return JSON.parse(text);
|
|
98
271
|
},
|
|
99
272
|
};
|
|
273
|
+
async #checkSignature(payload, signature, secret) {
|
|
274
|
+
const key = secret ?? this.#webhookSecret;
|
|
275
|
+
if (!key) {
|
|
276
|
+
throw new Error('Skalp: missing webhook secret. Pass it as the third argument or set SKALP_WEBHOOK_SECRET.');
|
|
277
|
+
}
|
|
278
|
+
const encoder = new TextEncoder();
|
|
279
|
+
const body = toBytes(payload, encoder);
|
|
280
|
+
const parsed = parseSignature(readSignature(signature));
|
|
281
|
+
if (!parsed)
|
|
282
|
+
return 'invalid';
|
|
283
|
+
if (Math.abs(Date.now() / 1000 - parsed.timestamp) > SIGNATURE_TOLERANCE_SECONDS)
|
|
284
|
+
return 'stale';
|
|
285
|
+
const prefix = encoder.encode(`${parsed.timestamp}.`);
|
|
286
|
+
const signed = new Uint8Array(prefix.length + body.length);
|
|
287
|
+
signed.set(prefix);
|
|
288
|
+
signed.set(body, prefix.length);
|
|
289
|
+
const hmac = await crypto.subtle.importKey('raw', encoder.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
290
|
+
return (await crypto.subtle.verify('HMAC', hmac, parsed.signature, signed)) ? undefined : 'invalid';
|
|
291
|
+
}
|
|
100
292
|
async #request(method, path, { body, query, auth = true } = {}) {
|
|
101
293
|
if (auth && !this.#apiKey) {
|
|
102
294
|
throw new Error('Skalp: missing API key. Pass it to new Skalp(apiKey) or set SKALP_API_KEY.');
|
|
@@ -106,18 +298,24 @@ export class Skalp {
|
|
|
106
298
|
if (value !== undefined && value !== null)
|
|
107
299
|
url.searchParams.set(name, String(value));
|
|
108
300
|
}
|
|
109
|
-
const headers = { accept: 'application/json' };
|
|
301
|
+
const headers = { accept: 'application/json', 'user-agent': USER_AGENT };
|
|
110
302
|
if (auth)
|
|
111
303
|
headers['x-api-key'] = this.#apiKey;
|
|
112
304
|
if (body !== undefined)
|
|
113
305
|
headers['content-type'] = 'application/json';
|
|
114
306
|
for (let attempt = 0;; attempt++) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
307
|
+
let res;
|
|
308
|
+
try {
|
|
309
|
+
res = await this.#fetch(url, {
|
|
310
|
+
method,
|
|
311
|
+
headers,
|
|
312
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
313
|
+
signal: AbortSignal.timeout(this.#timeout),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
catch (cause) {
|
|
317
|
+
throw new SkalpConnectionError(method, path, cause);
|
|
318
|
+
}
|
|
121
319
|
if (res.status === 429 && attempt < this.#maxRetries) {
|
|
122
320
|
const wait = retryAfterSeconds(res, attempt);
|
|
123
321
|
if (wait <= MAX_RETRY_WAIT_SECONDS) {
|
|
@@ -182,7 +380,7 @@ function retryAfterSeconds(res, attempt) {
|
|
|
182
380
|
const reset = Number(res.headers.get('x-ratelimit-reset'));
|
|
183
381
|
if (reset > 0)
|
|
184
382
|
return Math.max(reset - Date.now() / 1000, 0.5);
|
|
185
|
-
return 2 ** attempt;
|
|
383
|
+
return 2 ** Math.min(attempt, 5);
|
|
186
384
|
}
|
|
187
385
|
function toError(status, body) {
|
|
188
386
|
const b = (body && typeof body === 'object' ? body : {});
|