@sneekin/sdk 0.3.2 → 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/dist/core/client.d.ts +24 -0
- package/dist/core/derive-keys.d.ts +6 -0
- package/dist/core/generated/api.d.ts +3490 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/index.cjs +223 -0
- package/dist/index.d.ts +2 -118
- package/dist/index.js +127 -129
- package/dist/messages/index.cjs +150 -0
- package/dist/messages/index.d.ts +1 -0
- package/dist/messages/index.js +121 -0
- package/dist/pay/index.cjs +223 -0
- package/dist/pay/index.d.ts +2 -0
- package/dist/pay/index.js +192 -0
- package/dist/pay/payments.d.ts +90 -0
- package/dist/verify/index.cjs +150 -0
- package/dist/verify/index.d.ts +1 -0
- package/dist/verify/index.js +121 -0
- package/package.json +67 -47
- package/README.md +0 -134
- package/dist/index.d.mts +0 -118
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -168
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { CreateOrderResponse, GetOrderResponse, Settlement as GeneratedSettlement, VerifyPayment, VerifyPaymentResponse } from '../core/generated/api.js';
|
|
2
|
+
import { SneekApiError, type SneekClientOptions } from '../core/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link SneekPayClient}.
|
|
5
|
+
*/
|
|
6
|
+
export type SneekPayClientOptions = SneekClientOptions;
|
|
7
|
+
export type PaymentStatus = GetOrderResponse['status'];
|
|
8
|
+
export interface CreateOrderInput {
|
|
9
|
+
/** Amount in MAJOR units (rupees), e.g. `499` or `'499.50'`. Max 2 decimals. */
|
|
10
|
+
amount: number | string;
|
|
11
|
+
/** ISO-4217 code. Defaults to `INR`. */
|
|
12
|
+
currency?: string;
|
|
13
|
+
/** Your own reference. Unique per application; a repeat returns the original order. */
|
|
14
|
+
receipt?: string;
|
|
15
|
+
/** Your correlation id, echoed back on reads. */
|
|
16
|
+
clientRef?: string;
|
|
17
|
+
/** Non-PII metadata forwarded to the gateway. */
|
|
18
|
+
notes?: Record<string, unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* When your event ends. The organizer share stays on hold until this moment
|
|
21
|
+
* plus the platform refund window, so buyer refunds can be reversed from it.
|
|
22
|
+
* Omit to hold for the refund window from payment only.
|
|
23
|
+
*/
|
|
24
|
+
settleAfter?: string | Date;
|
|
25
|
+
/** Sent as the `Idempotency-Key` header so a retried call cannot double-charge. */
|
|
26
|
+
idempotencyKey?: string;
|
|
27
|
+
}
|
|
28
|
+
export type SettlementStatus = GeneratedSettlement['status'];
|
|
29
|
+
/** Where the organizer share of a partner payment stands. Amounts in major units. */
|
|
30
|
+
export type Settlement = GeneratedSettlement;
|
|
31
|
+
export type CreateOrderResult = CreateOrderResponse;
|
|
32
|
+
export type PaymentOrder = GetOrderResponse;
|
|
33
|
+
/** Exactly the three fields the browser checkout handler hands back. */
|
|
34
|
+
export interface VerifyInput {
|
|
35
|
+
orderId: VerifyPayment['order_id'];
|
|
36
|
+
paymentId: VerifyPayment['payment_id'];
|
|
37
|
+
signature: VerifyPayment['signature'];
|
|
38
|
+
}
|
|
39
|
+
export type VerifyResult = VerifyPaymentResponse;
|
|
40
|
+
/**
|
|
41
|
+
* Accept payments with only your Sneek application API key.
|
|
42
|
+
*
|
|
43
|
+
* Sneek holds the payment-gateway merchant credentials centrally, exactly as it
|
|
44
|
+
* holds the SMS/WhatsApp/email provider credentials centrally. Your
|
|
45
|
+
* server never stores a gateway secret; the only gateway value you ever receive
|
|
46
|
+
* is `key_id`, the PUBLIC key the browser checkout widget needs.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const pay = new SneekPayClient({ apiUrl: 'https://sneek.in', apiKey });
|
|
50
|
+
*
|
|
51
|
+
* // 1. Server: create the order.
|
|
52
|
+
* const order = await pay.orders.create({ amount: '499.00', receipt: 'inv-1001' });
|
|
53
|
+
*
|
|
54
|
+
* // 2. Browser: open checkout with order.key_id and order.provider_order_id.
|
|
55
|
+
*
|
|
56
|
+
* // 3. Server: verify what the browser handler returned.
|
|
57
|
+
* const result = await pay.verify({
|
|
58
|
+
* orderId: handler.razorpay_order_id,
|
|
59
|
+
* paymentId: handler.razorpay_payment_id,
|
|
60
|
+
* signature: handler.razorpay_signature,
|
|
61
|
+
* });
|
|
62
|
+
*/
|
|
63
|
+
export declare class SneekPayClient {
|
|
64
|
+
private readonly client;
|
|
65
|
+
readonly orders: OrdersClient;
|
|
66
|
+
constructor(options: SneekPayClientOptions);
|
|
67
|
+
/** @internal */
|
|
68
|
+
request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
|
|
69
|
+
/**
|
|
70
|
+
* Verify the checkout handler payload and settle the order. Idempotent:
|
|
71
|
+
* calling it twice with the same pair returns the same result.
|
|
72
|
+
*/
|
|
73
|
+
verify(input: VerifyInput): Promise<VerifyResult>;
|
|
74
|
+
}
|
|
75
|
+
export declare class OrdersClient {
|
|
76
|
+
private readonly pay;
|
|
77
|
+
constructor(pay: SneekPayClient);
|
|
78
|
+
create(input: CreateOrderInput): Promise<CreateOrderResult>;
|
|
79
|
+
get(id: string): Promise<PaymentOrder>;
|
|
80
|
+
/**
|
|
81
|
+
* The event moved: hold the organizer share until the new end plus the
|
|
82
|
+
* platform refund window. Only works while the payout is still on hold;
|
|
83
|
+
* a released payout answers 409.
|
|
84
|
+
*/
|
|
85
|
+
reschedule(id: string, input: {
|
|
86
|
+
settleAfter: string | Date;
|
|
87
|
+
}): Promise<PaymentOrder>;
|
|
88
|
+
}
|
|
89
|
+
export { SneekApiError };
|
|
90
|
+
export default SneekPayClient;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/verify/index.ts
|
|
21
|
+
var verify_exports = {};
|
|
22
|
+
__export(verify_exports, {
|
|
23
|
+
SneekApiError: () => SneekApiError,
|
|
24
|
+
SneekClient: () => SneekClient,
|
|
25
|
+
deriveKeys: () => deriveKeys
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(verify_exports);
|
|
28
|
+
|
|
29
|
+
// src/core/client.ts
|
|
30
|
+
var SneekApiError = class extends Error {
|
|
31
|
+
constructor(status, type, title, detail) {
|
|
32
|
+
super(`${title}${detail ? `: ${detail}` : ""} [${status}]`);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.type = type;
|
|
35
|
+
this.title = title;
|
|
36
|
+
this.detail = detail;
|
|
37
|
+
this.name = "SneekApiError";
|
|
38
|
+
}
|
|
39
|
+
status;
|
|
40
|
+
type;
|
|
41
|
+
title;
|
|
42
|
+
detail;
|
|
43
|
+
};
|
|
44
|
+
var SneekClient = class {
|
|
45
|
+
apiUrl;
|
|
46
|
+
apiKey;
|
|
47
|
+
fetchImpl;
|
|
48
|
+
maxRetries;
|
|
49
|
+
retryBaseMs;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
if (!options.apiUrl) throw new Error("apiUrl is required");
|
|
52
|
+
if (!options.apiKey) throw new Error("apiKey is required");
|
|
53
|
+
this.apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
54
|
+
this.apiKey = options.apiKey;
|
|
55
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
56
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
57
|
+
this.retryBaseMs = options.retryBaseMs ?? 200;
|
|
58
|
+
}
|
|
59
|
+
async request(method, path, body, extraHeaders) {
|
|
60
|
+
const raw = body === void 0 ? void 0 : JSON.stringify(body);
|
|
61
|
+
let attempt = 0;
|
|
62
|
+
let lastError;
|
|
63
|
+
for (; ; ) {
|
|
64
|
+
try {
|
|
65
|
+
const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
|
|
66
|
+
method,
|
|
67
|
+
headers: {
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
70
|
+
...extraHeaders ?? {}
|
|
71
|
+
},
|
|
72
|
+
body: raw
|
|
73
|
+
});
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
const parsed = text ? parseJson(text) : void 0;
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const problem = asProblem(parsed);
|
|
78
|
+
const error = new SneekApiError(
|
|
79
|
+
response.status,
|
|
80
|
+
problem.type ?? "about:blank",
|
|
81
|
+
problem.title ?? response.statusText,
|
|
82
|
+
problem.detail
|
|
83
|
+
);
|
|
84
|
+
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
85
|
+
lastError = error;
|
|
86
|
+
await this.backoff(attempt++);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
return parsed;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof SneekApiError) throw error;
|
|
94
|
+
if (attempt < this.maxRetries) {
|
|
95
|
+
lastError = error;
|
|
96
|
+
await this.backoff(attempt++);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
throw lastError ?? error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
backoff(attempt) {
|
|
104
|
+
return new Promise((resolve) => setTimeout(resolve, this.retryBaseMs * 2 ** attempt));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function parseJson(text) {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(text);
|
|
110
|
+
} catch {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function asProblem(value) {
|
|
115
|
+
if (typeof value !== "object" || value === null) return {};
|
|
116
|
+
const problem = value;
|
|
117
|
+
return {
|
|
118
|
+
type: typeof problem.type === "string" ? problem.type : void 0,
|
|
119
|
+
title: typeof problem.title === "string" ? problem.title : void 0,
|
|
120
|
+
detail: typeof problem.detail === "string" ? problem.detail : void 0
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/core/derive-keys.ts
|
|
125
|
+
var import_node_crypto = require("crypto");
|
|
126
|
+
var KEY_LABELS = {
|
|
127
|
+
qrSigningKey: "sneek-qr-v1",
|
|
128
|
+
webhookSigningKey: "sneek-webhook-v1",
|
|
129
|
+
proofKey: "sneek-proof-v1"
|
|
130
|
+
};
|
|
131
|
+
function deriveKeys(apiKey) {
|
|
132
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
133
|
+
throw new TypeError("apiKey must be a nonempty string");
|
|
134
|
+
}
|
|
135
|
+
const inputKeyMaterial = Buffer.from(apiKey, "utf8");
|
|
136
|
+
const derive = (label) => Buffer.from(
|
|
137
|
+
(0, import_node_crypto.hkdfSync)("sha256", inputKeyMaterial, Buffer.alloc(0), label, 32)
|
|
138
|
+
);
|
|
139
|
+
return {
|
|
140
|
+
qrSigningKey: derive(KEY_LABELS.qrSigningKey),
|
|
141
|
+
webhookSigningKey: derive(KEY_LABELS.webhookSigningKey),
|
|
142
|
+
proofKey: derive(KEY_LABELS.proofKey)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
146
|
+
0 && (module.exports = {
|
|
147
|
+
SneekApiError,
|
|
148
|
+
SneekClient,
|
|
149
|
+
deriveKeys
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core/index.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/core/client.ts
|
|
2
|
+
var SneekApiError = class extends Error {
|
|
3
|
+
constructor(status, type, title, detail) {
|
|
4
|
+
super(`${title}${detail ? `: ${detail}` : ""} [${status}]`);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.type = type;
|
|
7
|
+
this.title = title;
|
|
8
|
+
this.detail = detail;
|
|
9
|
+
this.name = "SneekApiError";
|
|
10
|
+
}
|
|
11
|
+
status;
|
|
12
|
+
type;
|
|
13
|
+
title;
|
|
14
|
+
detail;
|
|
15
|
+
};
|
|
16
|
+
var SneekClient = class {
|
|
17
|
+
apiUrl;
|
|
18
|
+
apiKey;
|
|
19
|
+
fetchImpl;
|
|
20
|
+
maxRetries;
|
|
21
|
+
retryBaseMs;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
if (!options.apiUrl) throw new Error("apiUrl is required");
|
|
24
|
+
if (!options.apiKey) throw new Error("apiKey is required");
|
|
25
|
+
this.apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
26
|
+
this.apiKey = options.apiKey;
|
|
27
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
28
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
29
|
+
this.retryBaseMs = options.retryBaseMs ?? 200;
|
|
30
|
+
}
|
|
31
|
+
async request(method, path, body, extraHeaders) {
|
|
32
|
+
const raw = body === void 0 ? void 0 : JSON.stringify(body);
|
|
33
|
+
let attempt = 0;
|
|
34
|
+
let lastError;
|
|
35
|
+
for (; ; ) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
"content-type": "application/json",
|
|
41
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
42
|
+
...extraHeaders ?? {}
|
|
43
|
+
},
|
|
44
|
+
body: raw
|
|
45
|
+
});
|
|
46
|
+
const text = await response.text();
|
|
47
|
+
const parsed = text ? parseJson(text) : void 0;
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const problem = asProblem(parsed);
|
|
50
|
+
const error = new SneekApiError(
|
|
51
|
+
response.status,
|
|
52
|
+
problem.type ?? "about:blank",
|
|
53
|
+
problem.title ?? response.statusText,
|
|
54
|
+
problem.detail
|
|
55
|
+
);
|
|
56
|
+
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
57
|
+
lastError = error;
|
|
58
|
+
await this.backoff(attempt++);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof SneekApiError) throw error;
|
|
66
|
+
if (attempt < this.maxRetries) {
|
|
67
|
+
lastError = error;
|
|
68
|
+
await this.backoff(attempt++);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
throw lastError ?? error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
backoff(attempt) {
|
|
76
|
+
return new Promise((resolve) => setTimeout(resolve, this.retryBaseMs * 2 ** attempt));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function parseJson(text) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
} catch {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function asProblem(value) {
|
|
87
|
+
if (typeof value !== "object" || value === null) return {};
|
|
88
|
+
const problem = value;
|
|
89
|
+
return {
|
|
90
|
+
type: typeof problem.type === "string" ? problem.type : void 0,
|
|
91
|
+
title: typeof problem.title === "string" ? problem.title : void 0,
|
|
92
|
+
detail: typeof problem.detail === "string" ? problem.detail : void 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/core/derive-keys.ts
|
|
97
|
+
import { hkdfSync } from "crypto";
|
|
98
|
+
var KEY_LABELS = {
|
|
99
|
+
qrSigningKey: "sneek-qr-v1",
|
|
100
|
+
webhookSigningKey: "sneek-webhook-v1",
|
|
101
|
+
proofKey: "sneek-proof-v1"
|
|
102
|
+
};
|
|
103
|
+
function deriveKeys(apiKey) {
|
|
104
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
105
|
+
throw new TypeError("apiKey must be a nonempty string");
|
|
106
|
+
}
|
|
107
|
+
const inputKeyMaterial = Buffer.from(apiKey, "utf8");
|
|
108
|
+
const derive = (label) => Buffer.from(
|
|
109
|
+
hkdfSync("sha256", inputKeyMaterial, Buffer.alloc(0), label, 32)
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
qrSigningKey: derive(KEY_LABELS.qrSigningKey),
|
|
113
|
+
webhookSigningKey: derive(KEY_LABELS.webhookSigningKey),
|
|
114
|
+
proofKey: derive(KEY_LABELS.proofKey)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
SneekApiError,
|
|
119
|
+
SneekClient,
|
|
120
|
+
deriveKeys
|
|
121
|
+
};
|
package/package.json
CHANGED
|
@@ -1,49 +1,69 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
"name": "@sneekin/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Server-side SDK for Sneek verification, messaging, and payments",
|
|
8
|
+
"homepage": "https://sneek.in/docs",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://sneek.in/docs"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.cjs"
|
|
21
|
+
},
|
|
22
|
+
"./verify": {
|
|
23
|
+
"types": "./dist/verify/index.d.ts",
|
|
24
|
+
"import": "./dist/verify/index.js",
|
|
25
|
+
"require": "./dist/verify/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./messages": {
|
|
28
|
+
"types": "./dist/messages/index.d.ts",
|
|
29
|
+
"import": "./dist/messages/index.js",
|
|
30
|
+
"require": "./dist/messages/index.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./pay": {
|
|
33
|
+
"types": "./dist/pay/index.d.ts",
|
|
34
|
+
"import": "./dist/pay/index.js",
|
|
35
|
+
"require": "./dist/pay/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup && tsc -p tsconfig.json --emitDeclarationOnly",
|
|
44
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
45
|
+
"test": "node --test --import tsx test/*.test.ts test/*.test.mjs",
|
|
46
|
+
"test:package": "node --test test/package-built.test.mjs"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"sneek",
|
|
50
|
+
"verification",
|
|
51
|
+
"messaging",
|
|
52
|
+
"payments"
|
|
53
|
+
],
|
|
54
|
+
"author": "Abblor Tech Pvt Ltd <abblorltd@gmail.com> (https://sneek.in)",
|
|
55
|
+
"license": "UNLICENSED",
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"jose": "6.2.8",
|
|
58
|
+
"zod": "3.25.76"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "22.19.17",
|
|
62
|
+
"tsup": "8.5.1",
|
|
63
|
+
"tsx": "4.21.0",
|
|
64
|
+
"typescript": "6.0.3"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20"
|
|
20
68
|
}
|
|
21
|
-
|
|
22
|
-
"files": [
|
|
23
|
-
"dist"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsup",
|
|
27
|
-
"type-check": "tsc --noEmit",
|
|
28
|
-
"test": "node --test --import tsx test/*.test.ts"
|
|
29
|
-
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
"sneek",
|
|
32
|
-
"messaging",
|
|
33
|
-
"sms",
|
|
34
|
-
"whatsapp",
|
|
35
|
-
"email",
|
|
36
|
-
"otp"
|
|
37
|
-
],
|
|
38
|
-
"author": "Abblor Tech Pvt Ltd <abblorltd@gmail.com> (https://sneek.in)",
|
|
39
|
-
"license": "UNLICENSED",
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@types/node": "^20.11.0",
|
|
42
|
-
"tsup": "^8.0.0",
|
|
43
|
-
"tsx": "^4.7.0",
|
|
44
|
-
"typescript": "^5.3.3"
|
|
45
|
-
},
|
|
46
|
-
"engines": {
|
|
47
|
-
"node": ">=18"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
69
|
+
}
|
package/README.md
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# @sneekin/sdk
|
|
2
|
-
|
|
3
|
-
Node.js SDK for the **Sneek Send** API — deliver OTPs and transactional
|
|
4
|
-
messages over SMS, WhatsApp, and email through Sneek's pre-approved DLT
|
|
5
|
-
sender, WhatsApp BSP number, and warmed email domain.
|
|
6
|
-
|
|
7
|
-
> Sneek **delivers**; your app generates and verifies its own OTP codes.
|
|
8
|
-
|
|
9
|
-
## Installation
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install @sneekin/sdk
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Quick start
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { Sneek } from '@sneekin/sdk';
|
|
19
|
-
|
|
20
|
-
const sneek = new Sneek({
|
|
21
|
-
apiUrl: process.env.SNEEK_API_URL ?? 'https://api.sneek.in',
|
|
22
|
-
apiKey: process.env.SNEEK_API_KEY!, // snk_live_… or snk_test_…
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// Generic send (body OR template)
|
|
26
|
-
await sneek.messages.send({
|
|
27
|
-
to: '+919876543210',
|
|
28
|
-
body: 'Your OTP to login to ConfHub is 482917. Do not share it.',
|
|
29
|
-
channel: 'sms', // 'auto' | 'sms' | 'whatsapp' | 'email'
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Channel helpers
|
|
33
|
-
await sneek.sendSMS('+919876543210', 'Your code is 482917');
|
|
34
|
-
await sneek.sendWhatsApp('+919876543210', 'Your code is 482917');
|
|
35
|
-
await sneek.sendEmail('user@example.com', 'Your code is 482917');
|
|
36
|
-
|
|
37
|
-
// OTP body-formatting helper (you generate the code yourself)
|
|
38
|
-
await sneek.sendOTP({
|
|
39
|
-
to: '+919876543210',
|
|
40
|
-
code: '482917',
|
|
41
|
-
appName: 'ConfHub',
|
|
42
|
-
channel: 'auto',
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// DLT/provider templates (server-rendered)
|
|
46
|
-
await sneek.messages.send({
|
|
47
|
-
to: '+919876543210',
|
|
48
|
-
channel: 'sms',
|
|
49
|
-
template: 'otp_login',
|
|
50
|
-
variables: { appName: 'ConfHub', otp: '482917' },
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Message intent (`type`)
|
|
55
|
-
|
|
56
|
-
Declare whether a message is an OTP or a plain communication so Sneek picks the
|
|
57
|
-
correct approved provider template — instead of guessing from the body text:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
// Auth / OTP template
|
|
61
|
-
await sneek.messages.send({ to, body: 'Your code is 482917', type: 'otp' });
|
|
62
|
-
|
|
63
|
-
// Utility / communication template
|
|
64
|
-
await sneek.messages.send({
|
|
65
|
-
to,
|
|
66
|
-
body: 'Your booking BK-7781 is confirmed.',
|
|
67
|
-
type: 'transactional',
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
`type` accepts `'otp'` or `'transactional'`. When omitted, Sneek infers intent
|
|
72
|
-
from the body (which can misclassify digit groups like `BK-7781` as an OTP), so
|
|
73
|
-
**passing `type` explicitly is recommended.** `sendOTP(...)` always tags
|
|
74
|
-
`type: 'otp'` for you.
|
|
75
|
-
|
|
76
|
-
## Authentication
|
|
77
|
-
|
|
78
|
-
Every request is sent with `Authorization: Bearer <apiKey>`. Mint keys from the
|
|
79
|
-
Sneek admin (`/o/<org>/applications/<id>` → API keys). Keys are shown **once**;
|
|
80
|
-
store them in your environment / secret manager. `snk_test_…` keys hit dev
|
|
81
|
-
adapters; `snk_live_…` keys deliver for real.
|
|
82
|
-
|
|
83
|
-
## Errors & retries
|
|
84
|
-
|
|
85
|
-
- Non-2xx responses throw `SneekApiError` with `{ status, type, title, detail }`
|
|
86
|
-
(RFC-7807 problem-details).
|
|
87
|
-
- Transient `5xx` and network failures are retried with exponential backoff
|
|
88
|
-
(`maxRetries`, default 2; `retryBaseMs`, default 200ms).
|
|
89
|
-
|
|
90
|
-
## Idempotency & rate limits
|
|
91
|
-
|
|
92
|
-
Pass `idempotencyKey` to make a send safely repeatable — the first call is
|
|
93
|
-
processed and its result is replayed for any retry with the same key (per
|
|
94
|
-
application), so a dropped connection never sends twice. This pairs with the
|
|
95
|
-
SDK's automatic `5xx` retries.
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
await sneek.messages.send({
|
|
99
|
-
to: '+919876543210',
|
|
100
|
-
type: 'otp',
|
|
101
|
-
body: 'Your code is 482917',
|
|
102
|
-
idempotencyKey: 'order-4815-otp',
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Sends are rate-limited per application (your plan's per-minute / per-day quota),
|
|
107
|
-
per recipient, and per source IP. Exceeding a limit throws `SneekApiError` with
|
|
108
|
-
`status === 429`; the problem body carries `retry_after` (seconds).
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
import { SneekApiError } from '@sneekin/sdk';
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
await sneek.sendSMS('+91…', 'hi');
|
|
116
|
-
} catch (err) {
|
|
117
|
-
if (err instanceof SneekApiError && err.status === 401) {
|
|
118
|
-
// invalid / revoked key
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## API
|
|
124
|
-
|
|
125
|
-
| Method | Description |
|
|
126
|
-
| --- | --- |
|
|
127
|
-
| `messages.send(input)` | Send a message (body or template; optional `type` intent). Returns `202` accepted envelope. |
|
|
128
|
-
| `messages.get(id)` | Fetch a previously-sent message by id. |
|
|
129
|
-
| `sendSMS/sendWhatsApp/sendEmail(to, body, clientRef?)` | Channel-pinned helpers. |
|
|
130
|
-
| `sendOTP({ to, code, appName?, channel?, template?, variables? })` | Format + send an OTP body. |
|
|
131
|
-
|
|
132
|
-
## License
|
|
133
|
-
|
|
134
|
-
UNLICENSED — © Abblor Tech Pvt Ltd.
|