@skalpgg/sdk 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SDN Ventures LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # skalp
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.
4
+
5
+ Full documentation with Next.js, Express, NestJS, Hono and Fastify examples: https://skalp.gg/docs/sdk
6
+
7
+ ```sh
8
+ npm install @skalpgg/sdk
9
+ ```
10
+
11
+ Runs on Node 20+, Bun, Deno, Vercel Edge and Cloudflare Workers. No dependencies; ships ESM, CommonJS and types.
12
+
13
+ ## Quickstart
14
+
15
+ ```sh
16
+ SKALP_API_KEY=sk_live_xxx
17
+ SKALP_WEBHOOK_SECRET=your_webhook_secret
18
+ ```
19
+
20
+ ```ts
21
+ import { Skalp } from '@skalpgg/sdk'
22
+
23
+ const skalp = new Skalp()
24
+
25
+ const session = await skalp.checkout.create({
26
+ productId: 'prod_xxx',
27
+ customerEmail: 'buyer@example.com',
28
+ returnUrl: 'https://example.com/thanks',
29
+ })
30
+
31
+ console.log(session.url)
32
+ ```
33
+
34
+ Send the buyer to `session.url`, then confirm the payment from the `order.paid` webhook.
35
+
36
+ `new Skalp()` reads `SKALP_API_KEY` and `SKALP_WEBHOOK_SECRET`. You can also pass them in: `new Skalp('sk_live_xxx')` or `new Skalp({ apiKey, webhookSecret, baseUrl, timeout, maxRetries, fetch })`.
37
+
38
+ Create the key on the API keys page of your dashboard. Each key is bound to one store.
39
+
40
+ ## Methods
41
+
42
+ | Method | Scope |
43
+ | --- | --- |
44
+ | `checkout.create({ productId, customerEmail?, metadata?, couponCode?, returnUrl? })` | `checkout:write` |
45
+ | `checkout.get(id)` | `checkout:read` |
46
+ | `orders.get(id)` | `orders:read` |
47
+ | `products.list()` | `products:read` |
48
+ | `subscriptions.createCheckout({ productId \| productSlug, externalRef?, customerEmail?, currency?, ... })` | `checkout:write` |
49
+ | `subscriptions.getCheckout(id)` | `checkout:read` |
50
+ | `subscriptions.list({ externalRef?, status?, updatedSince?, limit?, offset? })` | `subscriptions:read` |
51
+ | `subscriptions.get(id)` | `subscriptions:read` |
52
+ | `subscriptions.cancel(id)` | `subscriptions:write` |
53
+ | `subscriptions.listInvoices(id)` | `subscriptions:read` |
54
+ | `licenses.verify({ licenseKey, serverIp, serverKey, serverPort? })` | none |
55
+ | `licenses.heartbeat({ licenseKey, serverIp })` | none |
56
+ | `webhooks.constructEvent(rawBody, headers)` | none |
57
+ | `webhooks.verify(rawBody, headers)` | none |
58
+
59
+ ## Webhooks
60
+
61
+ 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.
62
+
63
+ ```ts
64
+ export async function POST(request: Request) {
65
+ const event = await skalp.webhooks.constructEvent(await request.text(), request.headers).catch(() => null)
66
+ if (!event) return new Response('Invalid signature', { status: 400 })
67
+
68
+ if (event.type === 'order.paid') {
69
+ await grantAccess(event.data.buyerEmail, event.data.licenses ?? [])
70
+ }
71
+ return new Response(null, { status: 200 })
72
+ }
73
+ ```
74
+
75
+ 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`.
76
+
77
+ ## Errors
78
+
79
+ API errors throw `SkalpError` with `status`, `code` (for example `STORE_REQUIRED`) and the parsed `body`. A `429` is retried up to `maxRetries` times (default 2) when the limit resets within 10 seconds. An invalid webhook signature throws `SkalpSignatureError`.
80
+
81
+ 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.
@@ -0,0 +1,57 @@
1
+ import type { CheckoutSession, CheckoutSessionStatus, CreateCheckoutParams, CreateSubscriptionCheckoutParams, HeartbeatParams, Invoice, LicenseVerification, ListSubscriptionsParams, Order, Product, SkalpEvent, Subscription, SubscriptionCheckout, SubscriptionCheckoutStatus, SubscriptionList, VerifyLicenseParams } from './types.js';
2
+ export type * from './types.js';
3
+ export interface SkalpOptions {
4
+ apiKey?: string;
5
+ webhookSecret?: string;
6
+ baseUrl?: string;
7
+ timeout?: number;
8
+ maxRetries?: number;
9
+ fetch?: typeof fetch;
10
+ dangerouslyAllowBrowser?: boolean;
11
+ }
12
+ export type WebhookPayload = string | Uint8Array | ArrayBuffer;
13
+ export type SignatureSource = string | null | undefined | {
14
+ get(name: string): string | null;
15
+ } | Record<string, string | Array<string> | undefined>;
16
+ export declare class SkalpError extends Error {
17
+ readonly status: number;
18
+ readonly code: string | undefined;
19
+ readonly body: unknown;
20
+ constructor(message: string, status: number, code?: string, body?: unknown);
21
+ }
22
+ export declare class SkalpSignatureError extends Error {
23
+ constructor(message?: string);
24
+ }
25
+ export declare class Skalp {
26
+ #private;
27
+ constructor(options?: string | SkalpOptions);
28
+ readonly checkout: {
29
+ create: (params: CreateCheckoutParams) => Promise<CheckoutSession>;
30
+ get: (id: string) => Promise<CheckoutSessionStatus>;
31
+ };
32
+ readonly orders: {
33
+ get: (id: string) => Promise<Order>;
34
+ };
35
+ readonly products: {
36
+ list: () => Promise<Product[]>;
37
+ };
38
+ readonly subscriptions: {
39
+ createCheckout: (params: CreateSubscriptionCheckoutParams) => Promise<SubscriptionCheckout>;
40
+ getCheckout: (id: string) => Promise<SubscriptionCheckoutStatus>;
41
+ list: (params?: ListSubscriptionsParams) => Promise<SubscriptionList>;
42
+ get: (id: string) => Promise<Subscription>;
43
+ cancel: (id: string) => Promise<Subscription>;
44
+ listInvoices: (id: string) => Promise<Invoice[]>;
45
+ };
46
+ readonly licenses: {
47
+ verify: (params: VerifyLicenseParams) => Promise<LicenseVerification>;
48
+ heartbeat: (params: HeartbeatParams) => Promise<{
49
+ valid: boolean;
50
+ }>;
51
+ };
52
+ readonly webhooks: {
53
+ verify: (payload: WebhookPayload, signature: SignatureSource, secret?: string) => Promise<boolean>;
54
+ constructEvent: (payload: WebhookPayload, signature: SignatureSource, secret?: string) => Promise<SkalpEvent>;
55
+ };
56
+ }
57
+ export default Skalp;
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Skalp = exports.SkalpSignatureError = exports.SkalpError = void 0;
4
+ const SIGNATURE_HEADER = 'skalp-signature';
5
+ const SIGNATURE_TOLERANCE_SECONDS = 300;
6
+ class SkalpError extends Error {
7
+ status;
8
+ code;
9
+ body;
10
+ constructor(message, status, code, body) {
11
+ super(message);
12
+ this.name = 'SkalpError';
13
+ this.status = status;
14
+ this.code = code;
15
+ this.body = body;
16
+ }
17
+ }
18
+ exports.SkalpError = SkalpError;
19
+ class SkalpSignatureError extends Error {
20
+ constructor(message = 'Invalid Skalp webhook signature') {
21
+ super(message);
22
+ this.name = 'SkalpSignatureError';
23
+ }
24
+ }
25
+ exports.SkalpSignatureError = SkalpSignatureError;
26
+ const env = (name) => globalThis.process?.env?.[name];
27
+ const segment = (id) => encodeURIComponent(id);
28
+ const MAX_RETRY_WAIT_SECONDS = 10;
29
+ class Skalp {
30
+ #apiKey;
31
+ #webhookSecret;
32
+ #baseUrl;
33
+ #timeout;
34
+ #maxRetries;
35
+ #fetch;
36
+ constructor(options = {}) {
37
+ const o = typeof options === 'string' ? { apiKey: options } : options;
38
+ this.#apiKey = o.apiKey ?? env('SKALP_API_KEY');
39
+ this.#webhookSecret = o.webhookSecret ?? env('SKALP_WEBHOOK_SECRET');
40
+ this.#baseUrl = (o.baseUrl ?? env('SKALP_API_URL') ?? 'https://api.skalp.gg').replace(/\/+$/, '');
41
+ this.#timeout = o.timeout ?? 30_000;
42
+ this.#maxRetries = o.maxRetries ?? 2;
43
+ this.#fetch = o.fetch ?? ((...args) => fetch(...args));
44
+ if (this.#apiKey && 'window' in globalThis && 'document' in globalThis && !o.dangerouslyAllowBrowser) {
45
+ throw new Error('Skalp: your API key is a secret and must never reach the browser. Call the SDK from server code, or pass dangerouslyAllowBrowser: true if you really know what you are doing.');
46
+ }
47
+ }
48
+ checkout = {
49
+ create: (params) => {
50
+ const { productId, ...rest } = params;
51
+ return this.#request('POST', '/v1/checkout/sessions', {
52
+ body: { items: [{ productId, quantity: 1 }], ...rest },
53
+ });
54
+ },
55
+ get: (id) => this.#request('GET', `/v1/checkout/sessions/${segment(id)}`),
56
+ };
57
+ orders = {
58
+ get: (id) => this.#request('GET', `/v1/orders/${segment(id)}`),
59
+ };
60
+ products = {
61
+ list: () => this.#request('GET', '/v1/products'),
62
+ };
63
+ subscriptions = {
64
+ createCheckout: (params) => this.#request('POST', '/v1/subscriptions/checkout', { body: params }),
65
+ getCheckout: (id) => this.#request('GET', `/v1/subscriptions/checkout/${segment(id)}`),
66
+ list: (params = {}) => this.#request('GET', '/v1/subscriptions', {
67
+ query: {
68
+ ...params,
69
+ updatedSince: params.updatedSince instanceof Date ? params.updatedSince.toISOString() : params.updatedSince,
70
+ },
71
+ }),
72
+ get: (id) => this.#request('GET', `/v1/subscriptions/${segment(id)}`),
73
+ cancel: (id) => this.#request('DELETE', `/v1/subscriptions/${segment(id)}`),
74
+ listInvoices: (id) => this.#request('GET', `/v1/subscriptions/${segment(id)}/invoices`).then((r) => r.data),
75
+ };
76
+ licenses = {
77
+ verify: (params) => this.#request('POST', '/licenses/verify', { body: params, auth: false }),
78
+ heartbeat: (params) => this.#request('POST', '/licenses/heartbeat', { body: params, auth: false }),
79
+ };
80
+ webhooks = {
81
+ verify: async (payload, signature, secret) => {
82
+ const key = secret ?? this.#webhookSecret;
83
+ if (!key) {
84
+ throw new Error('Skalp: missing webhook secret. Pass it as the third argument or set SKALP_WEBHOOK_SECRET.');
85
+ }
86
+ const encoder = new TextEncoder();
87
+ const body = toBytes(payload, encoder);
88
+ const parsed = parseSignature(readSignature(signature));
89
+ if (!parsed || Math.abs(Date.now() / 1000 - parsed.timestamp) > SIGNATURE_TOLERANCE_SECONDS)
90
+ return false;
91
+ const prefix = encoder.encode(`${parsed.timestamp}.`);
92
+ const signed = new Uint8Array(prefix.length + body.length);
93
+ signed.set(prefix);
94
+ signed.set(body, prefix.length);
95
+ const hmac = await crypto.subtle.importKey('raw', encoder.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
96
+ return crypto.subtle.verify('HMAC', hmac, parsed.signature, signed);
97
+ },
98
+ constructEvent: async (payload, signature, secret) => {
99
+ if (!(await this.webhooks.verify(payload, signature, secret)))
100
+ throw new SkalpSignatureError();
101
+ const text = typeof payload === 'string' ? payload : new TextDecoder().decode(payload);
102
+ return JSON.parse(text);
103
+ },
104
+ };
105
+ async #request(method, path, { body, query, auth = true } = {}) {
106
+ if (auth && !this.#apiKey) {
107
+ throw new Error('Skalp: missing API key. Pass it to new Skalp(apiKey) or set SKALP_API_KEY.');
108
+ }
109
+ const url = new URL(this.#baseUrl + path);
110
+ for (const [name, value] of Object.entries(query ?? {})) {
111
+ if (value !== undefined && value !== null)
112
+ url.searchParams.set(name, String(value));
113
+ }
114
+ const headers = { accept: 'application/json' };
115
+ if (auth)
116
+ headers['x-api-key'] = this.#apiKey;
117
+ if (body !== undefined)
118
+ headers['content-type'] = 'application/json';
119
+ for (let attempt = 0;; attempt++) {
120
+ const res = await this.#fetch(url, {
121
+ method,
122
+ headers,
123
+ body: body === undefined ? undefined : JSON.stringify(body),
124
+ signal: AbortSignal.timeout(this.#timeout),
125
+ });
126
+ if (res.status === 429 && attempt < this.#maxRetries) {
127
+ const wait = retryAfterSeconds(res, attempt);
128
+ if (wait <= MAX_RETRY_WAIT_SECONDS) {
129
+ await res.body?.cancel();
130
+ await new Promise((resolve) => setTimeout(resolve, wait * 1000));
131
+ continue;
132
+ }
133
+ }
134
+ const text = await res.text();
135
+ let data = undefined;
136
+ try {
137
+ data = text ? JSON.parse(text) : undefined;
138
+ }
139
+ catch {
140
+ data = text;
141
+ }
142
+ if (!res.ok)
143
+ throw toError(res.status, data);
144
+ return data;
145
+ }
146
+ }
147
+ }
148
+ exports.Skalp = Skalp;
149
+ function readSignature(source) {
150
+ if (source == null || typeof source === 'string')
151
+ return source ?? undefined;
152
+ if (typeof source.get === 'function')
153
+ return source.get(SIGNATURE_HEADER) ?? undefined;
154
+ const record = source;
155
+ const value = record[SIGNATURE_HEADER] ?? record['Skalp-Signature'];
156
+ return Array.isArray(value) ? value[0] : value;
157
+ }
158
+ function parseSignature(header) {
159
+ let timestamp = NaN;
160
+ let hex = '';
161
+ for (const part of header?.split(',') ?? []) {
162
+ const [name, value = ''] = part.trim().split('=');
163
+ if (name === 't' && /^\d+$/.test(value))
164
+ timestamp = Number(value);
165
+ if (name === 'v1')
166
+ hex = value;
167
+ }
168
+ if (!Number.isSafeInteger(timestamp) || !/^[0-9a-f]{64}$/i.test(hex))
169
+ return undefined;
170
+ const signature = new Uint8Array(32);
171
+ for (let i = 0; i < 32; i++)
172
+ signature[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
173
+ return { timestamp, signature };
174
+ }
175
+ function toBytes(payload, encoder) {
176
+ if (typeof payload === 'string')
177
+ return encoder.encode(payload);
178
+ if (payload instanceof ArrayBuffer)
179
+ return new Uint8Array(payload);
180
+ if (payload instanceof Uint8Array)
181
+ return payload;
182
+ throw new TypeError('Skalp: webhook payload must be the raw request body (string, Buffer or ArrayBuffer). A parsed JSON object cannot be verified, because re-serializing it changes the bytes that were signed.');
183
+ }
184
+ function retryAfterSeconds(res, attempt) {
185
+ const retryAfter = Number(res.headers.get('retry-after'));
186
+ if (retryAfter > 0)
187
+ return retryAfter;
188
+ const reset = Number(res.headers.get('x-ratelimit-reset'));
189
+ if (reset > 0)
190
+ return Math.max(reset - Date.now() / 1000, 0.5);
191
+ return 2 ** attempt;
192
+ }
193
+ function toError(status, body) {
194
+ const b = (body && typeof body === 'object' ? body : {});
195
+ let message = typeof b.message === 'string' ? b.message : typeof body === 'string' && body ? body.slice(0, 200) : '';
196
+ if (message.startsWith('{')) {
197
+ try {
198
+ const inner = JSON.parse(message);
199
+ if (Array.isArray(inner.message))
200
+ message = inner.message.join('; ');
201
+ }
202
+ catch { }
203
+ }
204
+ const code = typeof b.code === 'string' ? b.code : undefined;
205
+ return new SkalpError(message || `Skalp API responded with ${status}`, status, code, body);
206
+ }
207
+ exports.default = Skalp;
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,252 @@
1
+ export type Currency = 'EUR' | 'USD' | 'PLN' | (string & {});
2
+ export type FulfillmentType = 'FILE' | 'SERIAL' | 'GAME' | 'SERVICE';
3
+ export type ProductType = 'ONE_TIME' | 'RECURRING' | 'COINS';
4
+ export type BillingInterval = 'WEEK' | 'MONTH' | 'YEAR';
5
+ export type OrderStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'REFUNDED' | 'DISPUTED';
6
+ export type SubscriptionStatus = 'ACTIVE' | 'CANCELED' | 'PAST_DUE' | 'UNPAID' | 'INCOMPLETE' | 'INCOMPLETE_EXPIRED' | 'TRIALING' | 'PAUSED';
7
+ export type Metadata = Record<string, string>;
8
+ export interface CreateCheckoutParams {
9
+ productId: string;
10
+ customerEmail?: string;
11
+ metadata?: Metadata;
12
+ couponCode?: string;
13
+ returnUrl?: string;
14
+ }
15
+ export interface CheckoutSession {
16
+ id: string;
17
+ url: string;
18
+ expiresAt: string;
19
+ status: 'pending';
20
+ }
21
+ export interface CheckoutSessionStatus {
22
+ id: string;
23
+ status: 'pending' | 'completed' | 'failed';
24
+ orderNumber: string;
25
+ amount: number;
26
+ currency: Currency;
27
+ completedAt: string | null;
28
+ metadata: Metadata;
29
+ }
30
+ export interface Order {
31
+ id: string;
32
+ orderNumber: string;
33
+ status: OrderStatus;
34
+ amount: number;
35
+ netAmount: number | null;
36
+ vatAmount: number | null;
37
+ currency: Currency;
38
+ productId: string;
39
+ productName: string;
40
+ productSlug: string;
41
+ fulfillmentType: FulfillmentType;
42
+ buyerEmail: string;
43
+ serialsCount?: number;
44
+ createdAt: string;
45
+ completedAt: string | null;
46
+ metadata: Metadata;
47
+ }
48
+ export interface Product {
49
+ id: string;
50
+ name: string;
51
+ slug: string;
52
+ price: number;
53
+ currency: Currency;
54
+ fulfillmentType: FulfillmentType;
55
+ productType: ProductType;
56
+ billingInterval: BillingInterval | null;
57
+ trialPeriodDays: number;
58
+ stock?: number;
59
+ }
60
+ export interface CreateSubscriptionCheckoutParams {
61
+ productId?: string;
62
+ productSlug?: string;
63
+ customerEmail?: string;
64
+ externalRef?: string;
65
+ metadata?: Metadata;
66
+ currency?: 'EUR' | 'USD' | 'PLN';
67
+ returnUrl?: string;
68
+ }
69
+ export interface SubscriptionCheckout {
70
+ id: string;
71
+ url: string;
72
+ expiresAt: string;
73
+ status: 'pending';
74
+ }
75
+ export interface SubscriptionCheckoutStatus {
76
+ id: string;
77
+ status: 'pending' | 'completed' | 'expired';
78
+ subscriptionId: string | null;
79
+ externalRef: string | null;
80
+ expiresAt: string;
81
+ metadata: Metadata;
82
+ }
83
+ export interface Subscription {
84
+ id: string;
85
+ status: SubscriptionStatus;
86
+ active: boolean;
87
+ externalRef: string | null;
88
+ productId: string;
89
+ productSlug: string;
90
+ productName: string;
91
+ billingInterval: BillingInterval;
92
+ amount: number;
93
+ currency: Currency;
94
+ currentPeriodStart: string;
95
+ currentPeriodEnd: string;
96
+ cancelAtPeriodEnd: boolean;
97
+ canceledAt: string | null;
98
+ trialEnd: string | null;
99
+ buyerEmail: string;
100
+ metadata: Metadata;
101
+ createdAt: string;
102
+ updatedAt: string;
103
+ }
104
+ export interface ListSubscriptionsParams {
105
+ externalRef?: string;
106
+ status?: SubscriptionStatus;
107
+ updatedSince?: string | Date;
108
+ limit?: number;
109
+ offset?: number;
110
+ }
111
+ export interface SubscriptionList {
112
+ data: Array<Subscription>;
113
+ total: number;
114
+ limit: number;
115
+ offset: number;
116
+ }
117
+ export interface Invoice {
118
+ id: string;
119
+ number: string | null;
120
+ issuedAt: string;
121
+ dueAt: string | null;
122
+ total: number;
123
+ currency: Currency;
124
+ status: string | null;
125
+ hostedUrl: string | null;
126
+ pdfUrl: string | null;
127
+ }
128
+ export interface VerifyLicenseParams {
129
+ licenseKey: string;
130
+ serverIp: string;
131
+ serverKey: string;
132
+ serverPort?: number;
133
+ }
134
+ export type LicenseError = 'INVALID_FORMAT' | 'INVALID_LICENSE' | 'LICENSE_INACTIVE' | 'LICENSE_EXPIRED' | 'PRODUCT_INACTIVE' | 'SERVER_NOT_ACTIVATED' | 'SERVER_KEY_REQUIRED' | 'SERVER_KEY_MISMATCH' | 'RATE_LIMITED' | 'REPLAY_DETECTED' | (string & {});
135
+ export interface LicenseVerification {
136
+ valid: boolean;
137
+ error?: LicenseError;
138
+ message?: string;
139
+ activatedServers?: Array<string>;
140
+ license?: {
141
+ key: string;
142
+ product: string;
143
+ version: string | null;
144
+ expiresAt: string | null;
145
+ };
146
+ token?: string;
147
+ expiresAt?: string;
148
+ publicKey?: string;
149
+ }
150
+ export interface HeartbeatParams {
151
+ licenseKey: string;
152
+ serverIp: string;
153
+ }
154
+ interface Event<TType extends string, TData> {
155
+ type: TType;
156
+ timestamp: string;
157
+ data: TData;
158
+ }
159
+ export interface WebhookLicense {
160
+ licenseId: string;
161
+ licenseKey: string;
162
+ productId: string;
163
+ productSlug: string;
164
+ productName: string;
165
+ }
166
+ export interface OrderPaidData {
167
+ orderId: string;
168
+ orderNumber: string;
169
+ productId: string;
170
+ productName: string;
171
+ productSlug: string;
172
+ buyerId: string;
173
+ buyerEmail: string;
174
+ licenseId: string;
175
+ licenseKey: string;
176
+ licenses?: Array<WebhookLicense>;
177
+ serials?: Array<string>;
178
+ amount: number;
179
+ currency: Currency;
180
+ quantity: number;
181
+ coins?: number;
182
+ baseCoins?: number;
183
+ bonusCoins?: number;
184
+ idempotencyKey: string;
185
+ steamhex: string | null;
186
+ identifier: string | null;
187
+ metadata: Metadata;
188
+ deliveryType?: string;
189
+ deliveryCommand?: string;
190
+ }
191
+ export interface OrderRefundedData {
192
+ orderId: string;
193
+ orderNumber: string;
194
+ refundId: string;
195
+ amount: number;
196
+ reason: string | null;
197
+ }
198
+ export interface OrderDisputedData {
199
+ orderId: string;
200
+ orderNumber: string;
201
+ disputeId: string;
202
+ reason: string;
203
+ steamHex: string | null;
204
+ identifier: string | null;
205
+ coins?: number;
206
+ banned: boolean;
207
+ }
208
+ export interface AccountRestoreData {
209
+ identifier: string;
210
+ steamHex: string;
211
+ reason: string;
212
+ }
213
+ export interface SubscriptionEventData {
214
+ subscriptionId: string;
215
+ externalRef: string | null;
216
+ status: SubscriptionStatus;
217
+ active: boolean;
218
+ storeId: string;
219
+ productId: string;
220
+ productSlug: string;
221
+ productName: string;
222
+ buyerId: string;
223
+ billingInterval: BillingInterval;
224
+ amount: number;
225
+ currency: Currency;
226
+ currentPeriodStart: string;
227
+ currentPeriodEnd: string;
228
+ cancelAtPeriodEnd: boolean;
229
+ canceledAt: string | null;
230
+ trialEnd: string | null;
231
+ metadata: Metadata;
232
+ idempotencyKey: string;
233
+ }
234
+ export interface WebhookTestData {
235
+ message: string;
236
+ webhookId: string;
237
+ sample: Partial<OrderPaidData>;
238
+ }
239
+ export type SkalpEvent = Event<'order.paid' | 'purchase.completed', OrderPaidData> | Event<'order.refunded', OrderRefundedData> | Event<'order.disputed' | 'account.suspend', OrderDisputedData> | Event<'account.restore', AccountRestoreData> | Event<'subscription.created' | 'subscription.updated', SubscriptionEventData> | Event<'subscription.renewed', SubscriptionEventData & {
240
+ invoiceId: string;
241
+ amountPaid: number;
242
+ periodExtendedTo: string;
243
+ }> | Event<'subscription.past_due', SubscriptionEventData & {
244
+ invoiceId: string;
245
+ attemptCount: number;
246
+ nextPaymentAttempt: string | null;
247
+ }> | Event<'subscription.canceled', SubscriptionEventData & {
248
+ endedAt: string | null;
249
+ cancelReason: 'requested' | 'payment_failure' | 'stripe';
250
+ }> | Event<'webhook.test', WebhookTestData>;
251
+ export type SkalpEventType = SkalpEvent['type'];
252
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,57 @@
1
+ import type { CheckoutSession, CheckoutSessionStatus, CreateCheckoutParams, CreateSubscriptionCheckoutParams, HeartbeatParams, Invoice, LicenseVerification, ListSubscriptionsParams, Order, Product, SkalpEvent, Subscription, SubscriptionCheckout, SubscriptionCheckoutStatus, SubscriptionList, VerifyLicenseParams } from './types.js';
2
+ export type * from './types.js';
3
+ export interface SkalpOptions {
4
+ apiKey?: string;
5
+ webhookSecret?: string;
6
+ baseUrl?: string;
7
+ timeout?: number;
8
+ maxRetries?: number;
9
+ fetch?: typeof fetch;
10
+ dangerouslyAllowBrowser?: boolean;
11
+ }
12
+ export type WebhookPayload = string | Uint8Array | ArrayBuffer;
13
+ export type SignatureSource = string | null | undefined | {
14
+ get(name: string): string | null;
15
+ } | Record<string, string | Array<string> | undefined>;
16
+ export declare class SkalpError extends Error {
17
+ readonly status: number;
18
+ readonly code: string | undefined;
19
+ readonly body: unknown;
20
+ constructor(message: string, status: number, code?: string, body?: unknown);
21
+ }
22
+ export declare class SkalpSignatureError extends Error {
23
+ constructor(message?: string);
24
+ }
25
+ export declare class Skalp {
26
+ #private;
27
+ constructor(options?: string | SkalpOptions);
28
+ readonly checkout: {
29
+ create: (params: CreateCheckoutParams) => Promise<CheckoutSession>;
30
+ get: (id: string) => Promise<CheckoutSessionStatus>;
31
+ };
32
+ readonly orders: {
33
+ get: (id: string) => Promise<Order>;
34
+ };
35
+ readonly products: {
36
+ list: () => Promise<Product[]>;
37
+ };
38
+ readonly subscriptions: {
39
+ createCheckout: (params: CreateSubscriptionCheckoutParams) => Promise<SubscriptionCheckout>;
40
+ getCheckout: (id: string) => Promise<SubscriptionCheckoutStatus>;
41
+ list: (params?: ListSubscriptionsParams) => Promise<SubscriptionList>;
42
+ get: (id: string) => Promise<Subscription>;
43
+ cancel: (id: string) => Promise<Subscription>;
44
+ listInvoices: (id: string) => Promise<Invoice[]>;
45
+ };
46
+ readonly licenses: {
47
+ verify: (params: VerifyLicenseParams) => Promise<LicenseVerification>;
48
+ heartbeat: (params: HeartbeatParams) => Promise<{
49
+ valid: boolean;
50
+ }>;
51
+ };
52
+ readonly webhooks: {
53
+ verify: (payload: WebhookPayload, signature: SignatureSource, secret?: string) => Promise<boolean>;
54
+ constructEvent: (payload: WebhookPayload, signature: SignatureSource, secret?: string) => Promise<SkalpEvent>;
55
+ };
56
+ }
57
+ export default Skalp;
@@ -0,0 +1,201 @@
1
+ const SIGNATURE_HEADER = 'skalp-signature';
2
+ const SIGNATURE_TOLERANCE_SECONDS = 300;
3
+ export class SkalpError extends Error {
4
+ status;
5
+ code;
6
+ body;
7
+ constructor(message, status, code, body) {
8
+ super(message);
9
+ this.name = 'SkalpError';
10
+ this.status = status;
11
+ this.code = code;
12
+ this.body = body;
13
+ }
14
+ }
15
+ export class SkalpSignatureError extends Error {
16
+ constructor(message = 'Invalid Skalp webhook signature') {
17
+ super(message);
18
+ this.name = 'SkalpSignatureError';
19
+ }
20
+ }
21
+ const env = (name) => globalThis.process?.env?.[name];
22
+ const segment = (id) => encodeURIComponent(id);
23
+ const MAX_RETRY_WAIT_SECONDS = 10;
24
+ export class Skalp {
25
+ #apiKey;
26
+ #webhookSecret;
27
+ #baseUrl;
28
+ #timeout;
29
+ #maxRetries;
30
+ #fetch;
31
+ constructor(options = {}) {
32
+ const o = typeof options === 'string' ? { apiKey: options } : options;
33
+ this.#apiKey = o.apiKey ?? env('SKALP_API_KEY');
34
+ this.#webhookSecret = o.webhookSecret ?? env('SKALP_WEBHOOK_SECRET');
35
+ this.#baseUrl = (o.baseUrl ?? env('SKALP_API_URL') ?? 'https://api.skalp.gg').replace(/\/+$/, '');
36
+ this.#timeout = o.timeout ?? 30_000;
37
+ this.#maxRetries = o.maxRetries ?? 2;
38
+ this.#fetch = o.fetch ?? ((...args) => fetch(...args));
39
+ if (this.#apiKey && 'window' in globalThis && 'document' in globalThis && !o.dangerouslyAllowBrowser) {
40
+ throw new Error('Skalp: your API key is a secret and must never reach the browser. Call the SDK from server code, or pass dangerouslyAllowBrowser: true if you really know what you are doing.');
41
+ }
42
+ }
43
+ checkout = {
44
+ create: (params) => {
45
+ const { productId, ...rest } = params;
46
+ return this.#request('POST', '/v1/checkout/sessions', {
47
+ body: { items: [{ productId, quantity: 1 }], ...rest },
48
+ });
49
+ },
50
+ get: (id) => this.#request('GET', `/v1/checkout/sessions/${segment(id)}`),
51
+ };
52
+ orders = {
53
+ get: (id) => this.#request('GET', `/v1/orders/${segment(id)}`),
54
+ };
55
+ products = {
56
+ list: () => this.#request('GET', '/v1/products'),
57
+ };
58
+ subscriptions = {
59
+ createCheckout: (params) => this.#request('POST', '/v1/subscriptions/checkout', { body: params }),
60
+ getCheckout: (id) => this.#request('GET', `/v1/subscriptions/checkout/${segment(id)}`),
61
+ list: (params = {}) => this.#request('GET', '/v1/subscriptions', {
62
+ query: {
63
+ ...params,
64
+ updatedSince: params.updatedSince instanceof Date ? params.updatedSince.toISOString() : params.updatedSince,
65
+ },
66
+ }),
67
+ get: (id) => this.#request('GET', `/v1/subscriptions/${segment(id)}`),
68
+ cancel: (id) => this.#request('DELETE', `/v1/subscriptions/${segment(id)}`),
69
+ listInvoices: (id) => this.#request('GET', `/v1/subscriptions/${segment(id)}/invoices`).then((r) => r.data),
70
+ };
71
+ licenses = {
72
+ verify: (params) => this.#request('POST', '/licenses/verify', { body: params, auth: false }),
73
+ heartbeat: (params) => this.#request('POST', '/licenses/heartbeat', { body: params, auth: false }),
74
+ };
75
+ 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
+ },
93
+ constructEvent: async (payload, signature, secret) => {
94
+ if (!(await this.webhooks.verify(payload, signature, secret)))
95
+ throw new SkalpSignatureError();
96
+ const text = typeof payload === 'string' ? payload : new TextDecoder().decode(payload);
97
+ return JSON.parse(text);
98
+ },
99
+ };
100
+ async #request(method, path, { body, query, auth = true } = {}) {
101
+ if (auth && !this.#apiKey) {
102
+ throw new Error('Skalp: missing API key. Pass it to new Skalp(apiKey) or set SKALP_API_KEY.');
103
+ }
104
+ const url = new URL(this.#baseUrl + path);
105
+ for (const [name, value] of Object.entries(query ?? {})) {
106
+ if (value !== undefined && value !== null)
107
+ url.searchParams.set(name, String(value));
108
+ }
109
+ const headers = { accept: 'application/json' };
110
+ if (auth)
111
+ headers['x-api-key'] = this.#apiKey;
112
+ if (body !== undefined)
113
+ headers['content-type'] = 'application/json';
114
+ for (let attempt = 0;; attempt++) {
115
+ const res = await this.#fetch(url, {
116
+ method,
117
+ headers,
118
+ body: body === undefined ? undefined : JSON.stringify(body),
119
+ signal: AbortSignal.timeout(this.#timeout),
120
+ });
121
+ if (res.status === 429 && attempt < this.#maxRetries) {
122
+ const wait = retryAfterSeconds(res, attempt);
123
+ if (wait <= MAX_RETRY_WAIT_SECONDS) {
124
+ await res.body?.cancel();
125
+ await new Promise((resolve) => setTimeout(resolve, wait * 1000));
126
+ continue;
127
+ }
128
+ }
129
+ const text = await res.text();
130
+ let data = undefined;
131
+ try {
132
+ data = text ? JSON.parse(text) : undefined;
133
+ }
134
+ catch {
135
+ data = text;
136
+ }
137
+ if (!res.ok)
138
+ throw toError(res.status, data);
139
+ return data;
140
+ }
141
+ }
142
+ }
143
+ function readSignature(source) {
144
+ if (source == null || typeof source === 'string')
145
+ return source ?? undefined;
146
+ if (typeof source.get === 'function')
147
+ return source.get(SIGNATURE_HEADER) ?? undefined;
148
+ const record = source;
149
+ const value = record[SIGNATURE_HEADER] ?? record['Skalp-Signature'];
150
+ return Array.isArray(value) ? value[0] : value;
151
+ }
152
+ function parseSignature(header) {
153
+ let timestamp = NaN;
154
+ let hex = '';
155
+ for (const part of header?.split(',') ?? []) {
156
+ const [name, value = ''] = part.trim().split('=');
157
+ if (name === 't' && /^\d+$/.test(value))
158
+ timestamp = Number(value);
159
+ if (name === 'v1')
160
+ hex = value;
161
+ }
162
+ if (!Number.isSafeInteger(timestamp) || !/^[0-9a-f]{64}$/i.test(hex))
163
+ return undefined;
164
+ const signature = new Uint8Array(32);
165
+ for (let i = 0; i < 32; i++)
166
+ signature[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
167
+ return { timestamp, signature };
168
+ }
169
+ function toBytes(payload, encoder) {
170
+ if (typeof payload === 'string')
171
+ return encoder.encode(payload);
172
+ if (payload instanceof ArrayBuffer)
173
+ return new Uint8Array(payload);
174
+ if (payload instanceof Uint8Array)
175
+ return payload;
176
+ throw new TypeError('Skalp: webhook payload must be the raw request body (string, Buffer or ArrayBuffer). A parsed JSON object cannot be verified, because re-serializing it changes the bytes that were signed.');
177
+ }
178
+ function retryAfterSeconds(res, attempt) {
179
+ const retryAfter = Number(res.headers.get('retry-after'));
180
+ if (retryAfter > 0)
181
+ return retryAfter;
182
+ const reset = Number(res.headers.get('x-ratelimit-reset'));
183
+ if (reset > 0)
184
+ return Math.max(reset - Date.now() / 1000, 0.5);
185
+ return 2 ** attempt;
186
+ }
187
+ function toError(status, body) {
188
+ const b = (body && typeof body === 'object' ? body : {});
189
+ let message = typeof b.message === 'string' ? b.message : typeof body === 'string' && body ? body.slice(0, 200) : '';
190
+ if (message.startsWith('{')) {
191
+ try {
192
+ const inner = JSON.parse(message);
193
+ if (Array.isArray(inner.message))
194
+ message = inner.message.join('; ');
195
+ }
196
+ catch { }
197
+ }
198
+ const code = typeof b.code === 'string' ? b.code : undefined;
199
+ return new SkalpError(message || `Skalp API responded with ${status}`, status, code, body);
200
+ }
201
+ export default Skalp;
@@ -0,0 +1,252 @@
1
+ export type Currency = 'EUR' | 'USD' | 'PLN' | (string & {});
2
+ export type FulfillmentType = 'FILE' | 'SERIAL' | 'GAME' | 'SERVICE';
3
+ export type ProductType = 'ONE_TIME' | 'RECURRING' | 'COINS';
4
+ export type BillingInterval = 'WEEK' | 'MONTH' | 'YEAR';
5
+ export type OrderStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'REFUNDED' | 'DISPUTED';
6
+ export type SubscriptionStatus = 'ACTIVE' | 'CANCELED' | 'PAST_DUE' | 'UNPAID' | 'INCOMPLETE' | 'INCOMPLETE_EXPIRED' | 'TRIALING' | 'PAUSED';
7
+ export type Metadata = Record<string, string>;
8
+ export interface CreateCheckoutParams {
9
+ productId: string;
10
+ customerEmail?: string;
11
+ metadata?: Metadata;
12
+ couponCode?: string;
13
+ returnUrl?: string;
14
+ }
15
+ export interface CheckoutSession {
16
+ id: string;
17
+ url: string;
18
+ expiresAt: string;
19
+ status: 'pending';
20
+ }
21
+ export interface CheckoutSessionStatus {
22
+ id: string;
23
+ status: 'pending' | 'completed' | 'failed';
24
+ orderNumber: string;
25
+ amount: number;
26
+ currency: Currency;
27
+ completedAt: string | null;
28
+ metadata: Metadata;
29
+ }
30
+ export interface Order {
31
+ id: string;
32
+ orderNumber: string;
33
+ status: OrderStatus;
34
+ amount: number;
35
+ netAmount: number | null;
36
+ vatAmount: number | null;
37
+ currency: Currency;
38
+ productId: string;
39
+ productName: string;
40
+ productSlug: string;
41
+ fulfillmentType: FulfillmentType;
42
+ buyerEmail: string;
43
+ serialsCount?: number;
44
+ createdAt: string;
45
+ completedAt: string | null;
46
+ metadata: Metadata;
47
+ }
48
+ export interface Product {
49
+ id: string;
50
+ name: string;
51
+ slug: string;
52
+ price: number;
53
+ currency: Currency;
54
+ fulfillmentType: FulfillmentType;
55
+ productType: ProductType;
56
+ billingInterval: BillingInterval | null;
57
+ trialPeriodDays: number;
58
+ stock?: number;
59
+ }
60
+ export interface CreateSubscriptionCheckoutParams {
61
+ productId?: string;
62
+ productSlug?: string;
63
+ customerEmail?: string;
64
+ externalRef?: string;
65
+ metadata?: Metadata;
66
+ currency?: 'EUR' | 'USD' | 'PLN';
67
+ returnUrl?: string;
68
+ }
69
+ export interface SubscriptionCheckout {
70
+ id: string;
71
+ url: string;
72
+ expiresAt: string;
73
+ status: 'pending';
74
+ }
75
+ export interface SubscriptionCheckoutStatus {
76
+ id: string;
77
+ status: 'pending' | 'completed' | 'expired';
78
+ subscriptionId: string | null;
79
+ externalRef: string | null;
80
+ expiresAt: string;
81
+ metadata: Metadata;
82
+ }
83
+ export interface Subscription {
84
+ id: string;
85
+ status: SubscriptionStatus;
86
+ active: boolean;
87
+ externalRef: string | null;
88
+ productId: string;
89
+ productSlug: string;
90
+ productName: string;
91
+ billingInterval: BillingInterval;
92
+ amount: number;
93
+ currency: Currency;
94
+ currentPeriodStart: string;
95
+ currentPeriodEnd: string;
96
+ cancelAtPeriodEnd: boolean;
97
+ canceledAt: string | null;
98
+ trialEnd: string | null;
99
+ buyerEmail: string;
100
+ metadata: Metadata;
101
+ createdAt: string;
102
+ updatedAt: string;
103
+ }
104
+ export interface ListSubscriptionsParams {
105
+ externalRef?: string;
106
+ status?: SubscriptionStatus;
107
+ updatedSince?: string | Date;
108
+ limit?: number;
109
+ offset?: number;
110
+ }
111
+ export interface SubscriptionList {
112
+ data: Array<Subscription>;
113
+ total: number;
114
+ limit: number;
115
+ offset: number;
116
+ }
117
+ export interface Invoice {
118
+ id: string;
119
+ number: string | null;
120
+ issuedAt: string;
121
+ dueAt: string | null;
122
+ total: number;
123
+ currency: Currency;
124
+ status: string | null;
125
+ hostedUrl: string | null;
126
+ pdfUrl: string | null;
127
+ }
128
+ export interface VerifyLicenseParams {
129
+ licenseKey: string;
130
+ serverIp: string;
131
+ serverKey: string;
132
+ serverPort?: number;
133
+ }
134
+ export type LicenseError = 'INVALID_FORMAT' | 'INVALID_LICENSE' | 'LICENSE_INACTIVE' | 'LICENSE_EXPIRED' | 'PRODUCT_INACTIVE' | 'SERVER_NOT_ACTIVATED' | 'SERVER_KEY_REQUIRED' | 'SERVER_KEY_MISMATCH' | 'RATE_LIMITED' | 'REPLAY_DETECTED' | (string & {});
135
+ export interface LicenseVerification {
136
+ valid: boolean;
137
+ error?: LicenseError;
138
+ message?: string;
139
+ activatedServers?: Array<string>;
140
+ license?: {
141
+ key: string;
142
+ product: string;
143
+ version: string | null;
144
+ expiresAt: string | null;
145
+ };
146
+ token?: string;
147
+ expiresAt?: string;
148
+ publicKey?: string;
149
+ }
150
+ export interface HeartbeatParams {
151
+ licenseKey: string;
152
+ serverIp: string;
153
+ }
154
+ interface Event<TType extends string, TData> {
155
+ type: TType;
156
+ timestamp: string;
157
+ data: TData;
158
+ }
159
+ export interface WebhookLicense {
160
+ licenseId: string;
161
+ licenseKey: string;
162
+ productId: string;
163
+ productSlug: string;
164
+ productName: string;
165
+ }
166
+ export interface OrderPaidData {
167
+ orderId: string;
168
+ orderNumber: string;
169
+ productId: string;
170
+ productName: string;
171
+ productSlug: string;
172
+ buyerId: string;
173
+ buyerEmail: string;
174
+ licenseId: string;
175
+ licenseKey: string;
176
+ licenses?: Array<WebhookLicense>;
177
+ serials?: Array<string>;
178
+ amount: number;
179
+ currency: Currency;
180
+ quantity: number;
181
+ coins?: number;
182
+ baseCoins?: number;
183
+ bonusCoins?: number;
184
+ idempotencyKey: string;
185
+ steamhex: string | null;
186
+ identifier: string | null;
187
+ metadata: Metadata;
188
+ deliveryType?: string;
189
+ deliveryCommand?: string;
190
+ }
191
+ export interface OrderRefundedData {
192
+ orderId: string;
193
+ orderNumber: string;
194
+ refundId: string;
195
+ amount: number;
196
+ reason: string | null;
197
+ }
198
+ export interface OrderDisputedData {
199
+ orderId: string;
200
+ orderNumber: string;
201
+ disputeId: string;
202
+ reason: string;
203
+ steamHex: string | null;
204
+ identifier: string | null;
205
+ coins?: number;
206
+ banned: boolean;
207
+ }
208
+ export interface AccountRestoreData {
209
+ identifier: string;
210
+ steamHex: string;
211
+ reason: string;
212
+ }
213
+ export interface SubscriptionEventData {
214
+ subscriptionId: string;
215
+ externalRef: string | null;
216
+ status: SubscriptionStatus;
217
+ active: boolean;
218
+ storeId: string;
219
+ productId: string;
220
+ productSlug: string;
221
+ productName: string;
222
+ buyerId: string;
223
+ billingInterval: BillingInterval;
224
+ amount: number;
225
+ currency: Currency;
226
+ currentPeriodStart: string;
227
+ currentPeriodEnd: string;
228
+ cancelAtPeriodEnd: boolean;
229
+ canceledAt: string | null;
230
+ trialEnd: string | null;
231
+ metadata: Metadata;
232
+ idempotencyKey: string;
233
+ }
234
+ export interface WebhookTestData {
235
+ message: string;
236
+ webhookId: string;
237
+ sample: Partial<OrderPaidData>;
238
+ }
239
+ export type SkalpEvent = Event<'order.paid' | 'purchase.completed', OrderPaidData> | Event<'order.refunded', OrderRefundedData> | Event<'order.disputed' | 'account.suspend', OrderDisputedData> | Event<'account.restore', AccountRestoreData> | Event<'subscription.created' | 'subscription.updated', SubscriptionEventData> | Event<'subscription.renewed', SubscriptionEventData & {
240
+ invoiceId: string;
241
+ amountPaid: number;
242
+ periodExtendedTo: string;
243
+ }> | Event<'subscription.past_due', SubscriptionEventData & {
244
+ invoiceId: string;
245
+ attemptCount: number;
246
+ nextPaymentAttempt: string | null;
247
+ }> | Event<'subscription.canceled', SubscriptionEventData & {
248
+ endedAt: string | null;
249
+ cancelReason: 'requested' | 'payment_failure' | 'stripe';
250
+ }> | Event<'webhook.test', WebhookTestData>;
251
+ export type SkalpEventType = SkalpEvent['type'];
252
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@skalpgg/sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official Skalp SDK for Node.js and edge runtimes: checkout, subscriptions, webhooks and license verification.",
5
+ "license": "MIT",
6
+ "publishConfig": { "access": "public" },
7
+ "homepage": "https://skalp.gg/docs/sdk",
8
+ "keywords": [
9
+ "skalp",
10
+ "payments",
11
+ "checkout",
12
+ "subscriptions",
13
+ "webhooks",
14
+ "licensing",
15
+ "sdk"
16
+ ],
17
+ "type": "module",
18
+ "main": "./dist/cjs/index.js",
19
+ "types": "./dist/cjs/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/esm/index.d.ts",
24
+ "default": "./dist/esm/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/cjs/index.d.ts",
28
+ "default": "./dist/cjs/index.js"
29
+ }
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "sideEffects": false,
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "scripts": {
40
+ "build": "rm -rf dist && tsc && tsc --module commonjs --moduleResolution bundler --outDir dist/cjs && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
41
+ "test": "npm run build && node --test test/*.test.mjs",
42
+ "prepublishOnly": "npm test"
43
+ },
44
+ "devDependencies": {
45
+ "typescript": "^7.0.2"
46
+ }
47
+ }