@sneekin/sdk 0.3.2 → 1.1.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.
@@ -0,0 +1,192 @@
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
+
118
+ // src/pay/payments.ts
119
+ var SneekPayClient = class {
120
+ client;
121
+ orders;
122
+ constructor(options) {
123
+ this.client = new SneekClient(options);
124
+ this.orders = new OrdersClient(this);
125
+ }
126
+ /** @internal */
127
+ async request(method, path, body, extraHeaders) {
128
+ return this.client.request(method, path, body, extraHeaders);
129
+ }
130
+ /**
131
+ * Verify the checkout handler payload and settle the order. Idempotent:
132
+ * calling it twice with the same pair returns the same result.
133
+ */
134
+ verify(input) {
135
+ return this.request("POST", "/api/payments/verify", {
136
+ order_id: input.orderId,
137
+ payment_id: input.paymentId,
138
+ signature: input.signature
139
+ });
140
+ }
141
+ };
142
+ var OrdersClient = class {
143
+ constructor(pay) {
144
+ this.pay = pay;
145
+ }
146
+ pay;
147
+ create(input) {
148
+ const payload = {
149
+ amount: String(input.amount),
150
+ currency: input.currency ?? "INR"
151
+ };
152
+ if (input.receipt !== void 0) payload.receipt = input.receipt;
153
+ if (input.clientRef !== void 0) payload.client_ref = input.clientRef;
154
+ if (input.notes !== void 0) payload.notes = input.notes;
155
+ if (input.settleAfter !== void 0) {
156
+ payload.settle_after = input.settleAfter instanceof Date ? input.settleAfter.toISOString() : input.settleAfter;
157
+ }
158
+ const headers = input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0;
159
+ return this.pay.request(
160
+ "POST",
161
+ "/api/payments/orders",
162
+ payload,
163
+ headers
164
+ );
165
+ }
166
+ get(id) {
167
+ return this.pay.request(
168
+ "GET",
169
+ `/api/payments/orders/${encodeURIComponent(id)}`
170
+ );
171
+ }
172
+ /**
173
+ * The event moved: hold the organizer share until the new end plus the
174
+ * platform refund window. Only works while the payout is still on hold;
175
+ * a released payout answers 409.
176
+ */
177
+ reschedule(id, input) {
178
+ const settleAfter = input.settleAfter instanceof Date ? input.settleAfter.toISOString() : input.settleAfter;
179
+ return this.pay.request(
180
+ "PATCH",
181
+ `/api/payments/orders/${encodeURIComponent(id)}/settlement`,
182
+ { settle_after: settleAfter }
183
+ );
184
+ }
185
+ };
186
+ export {
187
+ OrdersClient,
188
+ SneekApiError,
189
+ SneekClient,
190
+ SneekPayClient,
191
+ deriveKeys
192
+ };
@@ -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,22 @@
1
+ export interface SneekQrSession {
2
+ ref: string;
3
+ applicationId: string;
4
+ origin: string;
5
+ bindingProofHash: string;
6
+ expiresAt: number;
7
+ requestId?: string;
8
+ }
9
+ export interface SneekQrApprovalEvent {
10
+ type: 'qr.approved';
11
+ applicationId: string;
12
+ ref: string;
13
+ requestId: string;
14
+ }
15
+ export interface SneekApprovalStore {
16
+ createQrSession(session: SneekQrSession): Promise<void>;
17
+ /** Atomically attach only the first matching approval to a live QR session. */
18
+ recordQrApproval(event: SneekQrApprovalEvent, now: number): Promise<'recorded' | 'duplicate' | 'missing'>;
19
+ readQrSession(ref: string): Promise<SneekQrSession | null>;
20
+ /** Atomically claim a token jti until its exp time; false means replay. */
21
+ claimJti(jti: string, expiresAt: number): Promise<boolean>;
22
+ }
@@ -0,0 +1,97 @@
1
+ import { z } from 'zod';
2
+ export declare const QrWebhookEventSchema: z.ZodObject<{
3
+ type: z.ZodLiteral<"qr.approved">;
4
+ applicationId: z.ZodString;
5
+ ref: z.ZodString;
6
+ requestId: z.ZodString;
7
+ }, "strict", z.ZodTypeAny, {
8
+ type: "qr.approved";
9
+ requestId: string;
10
+ ref: string;
11
+ applicationId: string;
12
+ }, {
13
+ type: "qr.approved";
14
+ requestId: string;
15
+ ref: string;
16
+ applicationId: string;
17
+ }>;
18
+ export declare const SneekVerifyActionSchema: z.ZodUnion<[z.ZodObject<{
19
+ action: z.ZodLiteral<"config">;
20
+ }, "strict", z.ZodTypeAny, {
21
+ action: "config";
22
+ }, {
23
+ action: "config";
24
+ }>, z.ZodObject<{
25
+ action: z.ZodLiteral<"request">;
26
+ identifier: z.ZodString;
27
+ }, "strict", z.ZodTypeAny, {
28
+ action: "request";
29
+ identifier: string;
30
+ }, {
31
+ action: "request";
32
+ identifier: string;
33
+ }>, z.ZodObject<{
34
+ action: z.ZodLiteral<"switch">;
35
+ requestId: z.ZodString;
36
+ channel: z.ZodEnum<["sms", "whatsapp", "email", "push"]>;
37
+ proof: z.ZodString;
38
+ }, "strict", z.ZodTypeAny, {
39
+ action: "switch";
40
+ requestId: string;
41
+ channel: "email" | "sms" | "whatsapp" | "push";
42
+ proof: string;
43
+ }, {
44
+ action: "switch";
45
+ requestId: string;
46
+ channel: "email" | "sms" | "whatsapp" | "push";
47
+ proof: string;
48
+ }>, z.ZodObject<{
49
+ action: z.ZodLiteral<"verify">;
50
+ requestId: z.ZodString;
51
+ code: z.ZodOptional<z.ZodString>;
52
+ proof: z.ZodString;
53
+ }, "strict", z.ZodTypeAny, {
54
+ action: "verify";
55
+ requestId: string;
56
+ proof: string;
57
+ code?: string | undefined;
58
+ }, {
59
+ action: "verify";
60
+ requestId: string;
61
+ proof: string;
62
+ code?: string | undefined;
63
+ }>, z.ZodObject<{
64
+ action: z.ZodLiteral<"verify">;
65
+ requestId: z.ZodString;
66
+ ref: z.ZodString;
67
+ bindingProof: z.ZodString;
68
+ }, "strict", z.ZodTypeAny, {
69
+ action: "verify";
70
+ requestId: string;
71
+ ref: string;
72
+ bindingProof: string;
73
+ }, {
74
+ action: "verify";
75
+ requestId: string;
76
+ ref: string;
77
+ bindingProof: string;
78
+ }>, z.ZodObject<{
79
+ action: z.ZodLiteral<"create">;
80
+ }, "strict", z.ZodTypeAny, {
81
+ action: "create";
82
+ }, {
83
+ action: "create";
84
+ }>, z.ZodObject<{
85
+ action: z.ZodLiteral<"status">;
86
+ ref: z.ZodString;
87
+ bindingProof: z.ZodString;
88
+ }, "strict", z.ZodTypeAny, {
89
+ action: "status";
90
+ ref: string;
91
+ bindingProof: string;
92
+ }, {
93
+ action: "status";
94
+ ref: string;
95
+ bindingProof: string;
96
+ }>]>;
97
+ export type SneekVerifyAction = z.infer<typeof SneekVerifyActionSchema>;
@@ -0,0 +1,41 @@
1
+ import type { SneekApprovalStore } from './approval-store.js';
2
+ export interface SneekVerifyHandlerOptions {
3
+ apiUrl: string;
4
+ apiKey: string;
5
+ allowedOrigins: string[];
6
+ approvalStore: SneekApprovalStore;
7
+ onVerified: (result: {
8
+ identifier: string;
9
+ kind: 'email' | 'mobile';
10
+ method: string;
11
+ verifiedAt: string;
12
+ requestId: string;
13
+ }) => void | Promise<void>;
14
+ fetch?: typeof fetch;
15
+ }
16
+ export declare class SneekVerifyHandler {
17
+ private readonly client;
18
+ private readonly allowedOrigins;
19
+ private readonly keys;
20
+ private readonly approvalStore;
21
+ private readonly onVerified;
22
+ private readonly jwksByIssuer;
23
+ constructor(options: SneekVerifyHandlerOptions);
24
+ handle(request: Request): Promise<Response>;
25
+ handleWebhook(request: Request): Promise<Response>;
26
+ private getAllowedOrigin;
27
+ private config;
28
+ private startVerification;
29
+ private switchVerification;
30
+ private verify;
31
+ private verifiedClaims;
32
+ private startResponse;
33
+ private createProof;
34
+ private isValidProof;
35
+ private createQr;
36
+ private qrStatus;
37
+ private qrSession;
38
+ private partnerConfig;
39
+ private isValidWebhookSignature;
40
+ private hash;
41
+ }