signyu 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BN Habitat Pvt Ltd (SignYu)
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,193 @@
1
+ # SignYu Node.js SDK
2
+
3
+ Official Node.js SDK for the [SignYu](https://signyu.com) Aadhaar eSign API. Upload a PDF, add signers, and send it for legally valid Aadhaar OTP based eSignature under the IT Act 2000, all from your backend.
4
+
5
+ - Zero runtime dependencies (uses the built-in `fetch`, `FormData` and `Blob`)
6
+ - ESM and CommonJS builds with full TypeScript types
7
+ - Webhook signature verification with constant time comparison
8
+
9
+ API docs: https://signyu.com/docs. API overview and pricing: https://signyu.com/api (signatures from ₹15 each on credit packs of 10 or more).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install signyu
15
+ ```
16
+
17
+ Requires Node.js 18 or newer.
18
+
19
+ ## Quickstart
20
+
21
+ ```ts
22
+ import { readFileSync } from "node:fs";
23
+ import { SignYu } from "signyu";
24
+
25
+ const signyu = new SignYu({ apiKey: process.env.SIGNYU_API_KEY! });
26
+
27
+ // 1. Upload the PDF
28
+ const doc = await signyu.documents.create({
29
+ file: readFileSync("agreement.pdf"),
30
+ fileName: "agreement.pdf",
31
+ name: "Service Agreement",
32
+ });
33
+
34
+ // 2. Add signers (they sign in this order, up to 6 per document)
35
+ await signyu.documents.addSigners(doc.documentId, [
36
+ { name: "Asha Rao", phone: "9876543210", email: "asha@example.com" },
37
+ { name: "Vikram Nair", phone: "9812345678", email: "vikram@example.com" },
38
+ ]);
39
+
40
+ // 3. Send: deducts one credit per signer and emails each signer a link
41
+ const sent = await signyu.documents.send(doc.documentId);
42
+ console.log(sent.signers.map((s) => s.signUrl));
43
+
44
+ // 4. Check progress (or use webhooks)
45
+ const status = await signyu.documents.get(doc.documentId);
46
+ console.log(status.status); // PENDING, SENT or COMPLETED
47
+ ```
48
+
49
+ CommonJS works too: `const { SignYu } = require("signyu");`
50
+
51
+ Get your API key from the dashboard under Developers (https://signyu.com/app/developers). Keep it on your server and never ship it to a browser.
52
+
53
+ ## Configuration
54
+
55
+ ```ts
56
+ const signyu = new SignYu({
57
+ apiKey: "sk_live_...", // required
58
+ baseUrl: "https://signyu.com", // optional
59
+ timeoutMs: 60_000, // optional, per request
60
+ fetch: customFetch, // optional, defaults to globalThis.fetch
61
+ });
62
+ ```
63
+
64
+ ## Methods
65
+
66
+ ### `documents.create({ file, fileName?, name? })`
67
+
68
+ `POST /api/v1/documents`. Uploads a PDF (at most 10MB) and creates a document in `PENDING` state. `file` can be a `Buffer`, `Uint8Array`, `ArrayBuffer` or `Blob`. It is always sent as `application/pdf`. `fileName` defaults to `document.pdf`, and `name` defaults to the file name.
69
+
70
+ Returns `{ documentId, name, status }`.
71
+
72
+ ### `documents.list({ limit?, offset? })`
73
+
74
+ `GET /api/v1/documents`. Your documents, most recent first. `limit` is 1 to 100 (default 20), `offset` defaults to 0.
75
+
76
+ Returns `{ documents: [{ documentId, name, status, createdAt, signers: { total, signed } }], limit, offset }`.
77
+
78
+ ### `documents.get(documentId)`
79
+
80
+ `GET /api/v1/documents/{documentId}`. Status and per-signer progress. Once `COMPLETED`, `downloadUrl` is a temporary presigned link to the signed PDF and `certificateUrl` points at the certificate endpoint. Each signer's `signUrl` is `null` until the document is sent.
81
+
82
+ ### `documents.addSigners(documentId, signers)`
83
+
84
+ `POST /api/v1/documents/{documentId}/signers`. Only while the document is `PENDING`. Each signer needs `name`, `phone` (digits only, at least 10) and `email`. At most 6 signers per document.
85
+
86
+ Optional custom stamp placement (PDF points, origin at the bottom-left of the page, box at least 140 x 110, one box per page):
87
+
88
+ ```ts
89
+ await signyu.documents.addSigners(documentId, [
90
+ {
91
+ name: "Asha Rao",
92
+ phone: "9876543210",
93
+ email: "asha@example.com",
94
+ advanced: {
95
+ signaturePlacement: {
96
+ positions: [{ page: 2, x: 31, y: 257, width: 253, height: 110 }],
97
+ },
98
+ },
99
+ },
100
+ ]);
101
+ ```
102
+
103
+ Returns `{ signers: [{ signerId, name, email, signingOrder, advanced? }] }`.
104
+
105
+ ### `documents.send(documentId)`
106
+
107
+ `POST /api/v1/documents/{documentId}/send`. Deducts one credit per signer, marks the document `SENT`, emails each signer a signing link and returns the links. Call it once per document.
108
+
109
+ Returns `{ documentId, status: "SENT", creditsRemaining, signers: [{ signerId, name, email, signingOrder, signUrl }] }`.
110
+
111
+ ### `documents.getCertificate(documentId)`
112
+
113
+ `GET /api/v1/documents/{documentId}/certificate`. Returns the completion certificate and audit trail PDF as a `Uint8Array`. Only available once the document is `COMPLETED`, otherwise it throws a `SignYuError` with code `invalid_state` (409).
114
+
115
+ ```ts
116
+ import { writeFileSync } from "node:fs";
117
+ writeFileSync("certificate.pdf", await signyu.documents.getCertificate(documentId));
118
+ ```
119
+
120
+ ## Webhooks
121
+
122
+ SignYu sends `signer.signed` and `document.completed` events as a JSON `POST`. Each request carries an `X-SignSetu-Signature: sha256=<hex>` header, an HMAC-SHA256 of the raw request body keyed with your endpoint's signing secret (`whsec_...`, shown in the dashboard). Always verify against the raw body bytes, not a re-serialized object.
123
+
124
+ ```ts
125
+ import express from "express";
126
+ import { SignYu, WebhookSignatureError } from "signyu";
127
+
128
+ const signyu = new SignYu({ apiKey: process.env.SIGNYU_API_KEY! });
129
+ const app = express();
130
+
131
+ app.post("/webhooks/signyu", express.raw({ type: "application/json" }), (req, res) => {
132
+ try {
133
+ const event = signyu.webhooks.constructEvent(
134
+ req.body, // Buffer
135
+ req.header("X-SignSetu-Signature"),
136
+ process.env.SIGNYU_WEBHOOK_SECRET!
137
+ );
138
+ if (event.event === "document.completed") {
139
+ // event.certificateUrl, event.signers ...
140
+ }
141
+ res.sendStatus(200);
142
+ } catch (err) {
143
+ if (err instanceof WebhookSignatureError) return res.sendStatus(400);
144
+ throw err;
145
+ }
146
+ });
147
+ ```
148
+
149
+ The same helpers are exported directly: `verifySignature(rawBody, header, secret)` returns a boolean, and `constructEvent(rawBody, header, secret)` returns the typed event or throws `WebhookSignatureError`. Deliveries can repeat, so make your handler idempotent.
150
+
151
+ ## Errors
152
+
153
+ Every non-2xx response throws a `SignYuError`:
154
+
155
+ ```ts
156
+ import { SignYuError } from "signyu";
157
+
158
+ try {
159
+ await signyu.documents.send(documentId);
160
+ } catch (err) {
161
+ if (err instanceof SignYuError) {
162
+ console.log(err.status); // 402
163
+ console.log(err.code); // "insufficient_credits"
164
+ console.log(err.message); // "You need 2 credits to send this document."
165
+ }
166
+ }
167
+ ```
168
+
169
+ | Status | `code` | Meaning |
170
+ | --- | --- | --- |
171
+ | 400 | `invalid_content_type`, `invalid_request`, `invalid_file`, `no_signers`, `signer_limit_reached` | The request is invalid. |
172
+ | 401 | `unauthorized` | Missing or invalid API key. |
173
+ | 402 | `insufficient_credits` | Not enough credits to send. |
174
+ | 403 | `api_access_not_enabled` | The account has no API access subscription. |
175
+ | 404 | `not_found` | The document does not exist or is not yours. |
176
+ | 409 | `invalid_state` | Not allowed in the document's current state. |
177
+ | 413 | `file_too_large` | The PDF is over 10MB. |
178
+ | 429 | `rate_limited` | Too many requests, retry with backoff. |
179
+ | 500 | `internal_error` | Something went wrong on our side. |
180
+ | 0 | `connection_error`, `timeout` | The request never got a response. |
181
+
182
+ Retry `429` and `5xx` with backoff. Do not blindly retry `send`, since a successful send that timed out on your side would charge credits again.
183
+
184
+ ## Links
185
+
186
+ - Docs: https://signyu.com/docs
187
+ - API: https://signyu.com/api
188
+ - OpenAPI spec: https://signyu.com/openapi.yaml
189
+ - Support: contact@mail.signyu.com
190
+
191
+ ## License
192
+
193
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,266 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
24
+ EVENT_HEADER: () => EVENT_HEADER,
25
+ SIGNATURE_HEADER: () => SIGNATURE_HEADER,
26
+ SignYu: () => SignYu,
27
+ SignYuError: () => SignYuError,
28
+ VERSION: () => VERSION,
29
+ WebhookSignatureError: () => WebhookSignatureError,
30
+ computeSignature: () => computeSignature,
31
+ constructEvent: () => constructEvent,
32
+ verifySignature: () => verifySignature,
33
+ webhooks: () => webhooks
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/errors.ts
38
+ var SignYuError = class extends Error {
39
+ status;
40
+ code;
41
+ /** Parsed response body, when there was one. */
42
+ body;
43
+ constructor(opts) {
44
+ super(opts.message);
45
+ this.name = "SignYuError";
46
+ this.status = opts.status;
47
+ this.code = opts.code;
48
+ this.body = opts.body;
49
+ if (opts.cause !== void 0) {
50
+ this.cause = opts.cause;
51
+ }
52
+ }
53
+ };
54
+ var WebhookSignatureError = class extends Error {
55
+ constructor(message = "Webhook signature verification failed.") {
56
+ super(message);
57
+ this.name = "WebhookSignatureError";
58
+ }
59
+ };
60
+
61
+ // src/webhooks.ts
62
+ var import_node_crypto = require("crypto");
63
+ var SIGNATURE_HEADER = "X-SignSetu-Signature";
64
+ var EVENT_HEADER = "X-SignSetu-Event";
65
+ function toBuffer(payload) {
66
+ if (typeof payload === "string") return Buffer.from(payload, "utf8");
67
+ if (payload instanceof ArrayBuffer) return Buffer.from(new Uint8Array(payload));
68
+ return Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength);
69
+ }
70
+ function computeSignature(payload, secret) {
71
+ const hex = (0, import_node_crypto.createHmac)("sha256", secret).update(toBuffer(payload)).digest("hex");
72
+ return `sha256=${hex}`;
73
+ }
74
+ function verifySignature(payload, signatureHeader, secret) {
75
+ if (!signatureHeader || !secret) return false;
76
+ const expected = Buffer.from(computeSignature(payload, secret), "utf8");
77
+ const received = Buffer.from(signatureHeader.trim(), "utf8");
78
+ if (expected.length !== received.length) return false;
79
+ return (0, import_node_crypto.timingSafeEqual)(expected, received);
80
+ }
81
+ function constructEvent(payload, signatureHeader, secret) {
82
+ if (!verifySignature(payload, signatureHeader, secret)) {
83
+ throw new WebhookSignatureError();
84
+ }
85
+ return JSON.parse(toBuffer(payload).toString("utf8"));
86
+ }
87
+ var webhooks = {
88
+ SIGNATURE_HEADER,
89
+ EVENT_HEADER,
90
+ computeSignature,
91
+ verifySignature,
92
+ constructEvent
93
+ };
94
+
95
+ // src/version.ts
96
+ var VERSION = "0.1.0";
97
+
98
+ // src/client.ts
99
+ var DEFAULT_BASE_URL = "https://signyu.com";
100
+ function toPdfBlob(file) {
101
+ if (typeof Blob !== "undefined" && file instanceof Blob) {
102
+ return file.type === "application/pdf" ? file : new Blob([file], { type: "application/pdf" });
103
+ }
104
+ if (file instanceof ArrayBuffer) {
105
+ return new Blob([file], { type: "application/pdf" });
106
+ }
107
+ if (file instanceof Uint8Array) {
108
+ const copy = new Uint8Array(file.byteLength);
109
+ copy.set(file);
110
+ return new Blob([copy], { type: "application/pdf" });
111
+ }
112
+ throw new TypeError("file must be a Buffer, Uint8Array, ArrayBuffer or Blob.");
113
+ }
114
+ var Documents = class {
115
+ constructor(client) {
116
+ this.client = client;
117
+ }
118
+ client;
119
+ /** `POST /api/v1/documents`: upload a PDF and create a `PENDING` document. */
120
+ create(params) {
121
+ if (!params || params.file === void 0 || params.file === null) {
122
+ throw new TypeError("documents.create requires a `file`.");
123
+ }
124
+ const form = new FormData();
125
+ form.append("file", toPdfBlob(params.file), params.fileName ?? "document.pdf");
126
+ if (params.name !== void 0) form.append("name", params.name);
127
+ return this.client._request({
128
+ method: "POST",
129
+ path: "/api/v1/documents",
130
+ body: form
131
+ });
132
+ }
133
+ /** `GET /api/v1/documents`: your documents, most recent first. */
134
+ list(params = {}) {
135
+ return this.client._request({
136
+ method: "GET",
137
+ path: "/api/v1/documents",
138
+ query: { limit: params.limit, offset: params.offset }
139
+ });
140
+ }
141
+ /** `GET /api/v1/documents/{documentId}`: status and per-signer progress. */
142
+ get(documentId) {
143
+ return this.client._request({
144
+ method: "GET",
145
+ path: `/api/v1/documents/${encodeURIComponent(documentId)}`
146
+ });
147
+ }
148
+ /** `POST /api/v1/documents/{documentId}/signers`: add signers while `PENDING` (max 6 per document). */
149
+ addSigners(documentId, signers) {
150
+ return this.client._request({
151
+ method: "POST",
152
+ path: `/api/v1/documents/${encodeURIComponent(documentId)}/signers`,
153
+ json: { signers }
154
+ });
155
+ }
156
+ /**
157
+ * `POST /api/v1/documents/{documentId}/send`: deducts one credit per signer,
158
+ * emails signing links and returns them. Not idempotent: call it once.
159
+ */
160
+ send(documentId) {
161
+ return this.client._request({
162
+ method: "POST",
163
+ path: `/api/v1/documents/${encodeURIComponent(documentId)}/send`
164
+ });
165
+ }
166
+ /**
167
+ * `GET /api/v1/documents/{documentId}/certificate`: the completion
168
+ * certificate and audit trail PDF as raw bytes. Only available once the
169
+ * document is `COMPLETED` (otherwise `409 invalid_state`).
170
+ */
171
+ getCertificate(documentId) {
172
+ return this.client._request({
173
+ method: "GET",
174
+ path: `/api/v1/documents/${encodeURIComponent(documentId)}/certificate`,
175
+ expect: "binary"
176
+ });
177
+ }
178
+ };
179
+ var SignYu = class {
180
+ documents;
181
+ webhooks = webhooks;
182
+ baseUrl;
183
+ apiKey;
184
+ timeoutMs;
185
+ fetchImpl;
186
+ constructor(options) {
187
+ if (!options || !options.apiKey) {
188
+ throw new TypeError("SignYu requires an `apiKey`.");
189
+ }
190
+ this.apiKey = options.apiKey;
191
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
192
+ this.timeoutMs = options.timeoutMs ?? 6e4;
193
+ const f = options.fetch ?? globalThis.fetch;
194
+ if (typeof f !== "function") {
195
+ throw new TypeError("No global fetch found. SignYu requires Node.js 18 or newer.");
196
+ }
197
+ this.fetchImpl = f;
198
+ this.documents = new Documents(this);
199
+ }
200
+ /** @internal */
201
+ async _request(opts) {
202
+ const url = new URL(this.baseUrl + opts.path);
203
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
204
+ if (v !== void 0) url.searchParams.set(k, String(v));
205
+ }
206
+ const headers = {
207
+ Authorization: `Bearer ${this.apiKey}`,
208
+ Accept: opts.expect === "binary" ? "application/pdf" : "application/json",
209
+ "User-Agent": `signyu-node/${VERSION}`
210
+ };
211
+ let body = opts.body;
212
+ if (opts.json !== void 0) {
213
+ headers["Content-Type"] = "application/json";
214
+ body = JSON.stringify(opts.json);
215
+ }
216
+ let res;
217
+ try {
218
+ res = await this.fetchImpl(url.toString(), {
219
+ method: opts.method,
220
+ headers,
221
+ body,
222
+ signal: AbortSignal.timeout(this.timeoutMs)
223
+ });
224
+ } catch (err) {
225
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
226
+ throw new SignYuError({
227
+ status: 0,
228
+ code: isTimeout ? "timeout" : "connection_error",
229
+ message: isTimeout ? `Request timed out after ${this.timeoutMs}ms.` : `Could not reach the SignYu API: ${err instanceof Error ? err.message : String(err)}`,
230
+ cause: err
231
+ });
232
+ }
233
+ if (!res.ok) {
234
+ const text = await res.text().catch(() => "");
235
+ let parsed = void 0;
236
+ try {
237
+ parsed = text ? JSON.parse(text) : void 0;
238
+ } catch {
239
+ parsed = text;
240
+ }
241
+ const errBody = parsed && typeof parsed === "object" ? parsed : {};
242
+ const code = typeof errBody.error === "string" ? errBody.error : res.status === 429 ? "rate_limited" : "http_error";
243
+ const message = typeof errBody.message === "string" ? errBody.message : `SignYu API request failed with status ${res.status}.`;
244
+ throw new SignYuError({ status: res.status, code, message, body: parsed });
245
+ }
246
+ if (opts.expect === "binary") {
247
+ return new Uint8Array(await res.arrayBuffer());
248
+ }
249
+ return await res.json();
250
+ }
251
+ };
252
+ // Annotate the CommonJS export names for ESM import in node:
253
+ 0 && (module.exports = {
254
+ DEFAULT_BASE_URL,
255
+ EVENT_HEADER,
256
+ SIGNATURE_HEADER,
257
+ SignYu,
258
+ SignYuError,
259
+ VERSION,
260
+ WebhookSignatureError,
261
+ computeSignature,
262
+ constructEvent,
263
+ verifySignature,
264
+ webhooks
265
+ });
266
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/webhooks.ts","../src/version.ts","../src/client.ts"],"sourcesContent":["export { SignYu, DEFAULT_BASE_URL } from \"./client\";\nexport type {\n SignYuOptions,\n CreateDocumentParams,\n ListDocumentsParams,\n FileInput,\n Documents,\n} from \"./client\";\nexport { SignYuError, WebhookSignatureError } from \"./errors\";\nexport {\n webhooks,\n verifySignature,\n constructEvent,\n computeSignature,\n SIGNATURE_HEADER,\n EVENT_HEADER,\n} from \"./webhooks\";\nexport type { Webhooks, WebhookPayload } from \"./webhooks\";\nexport { VERSION } from \"./version\";\nexport * from \"./types\";\n","/**\n * Thrown for any non-2xx API response, network failure or timeout.\n *\n * `code` is the API's machine readable `error` field (for example\n * `insufficient_credits`). For responses without a JSON error body it is\n * `rate_limited` (429) or `http_error`; for network failures it is\n * `connection_error` and for timeouts `timeout`, both with `status` 0.\n */\nexport class SignYuError extends Error {\n readonly status: number;\n readonly code: string;\n /** Parsed response body, when there was one. */\n readonly body: unknown;\n\n constructor(opts: {\n status: number;\n code: string;\n message: string;\n body?: unknown;\n cause?: unknown;\n }) {\n super(opts.message);\n this.name = \"SignYuError\";\n this.status = opts.status;\n this.code = opts.code;\n this.body = opts.body;\n if (opts.cause !== undefined) {\n (this as { cause?: unknown }).cause = opts.cause;\n }\n }\n}\n\n/** Thrown by `webhooks.constructEvent` when the signature does not match. */\nexport class WebhookSignatureError extends Error {\n constructor(message = \"Webhook signature verification failed.\") {\n super(message);\n this.name = \"WebhookSignatureError\";\n }\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { WebhookSignatureError } from \"./errors\";\nimport type { WebhookEvent } from \"./types\";\n\n/** Header carrying the signature, `sha256=<hex>`. */\nexport const SIGNATURE_HEADER = \"X-SignSetu-Signature\";\n/** Header carrying the event type, for example `signer.signed`. */\nexport const EVENT_HEADER = \"X-SignSetu-Event\";\n\nexport type WebhookPayload = string | Uint8Array | ArrayBuffer;\n\nfunction toBuffer(payload: WebhookPayload): Buffer {\n if (typeof payload === \"string\") return Buffer.from(payload, \"utf8\");\n if (payload instanceof ArrayBuffer) return Buffer.from(new Uint8Array(payload));\n return Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength);\n}\n\n/** Computes the `sha256=<hex>` header value SignYu sends for a raw body. */\nexport function computeSignature(payload: WebhookPayload, secret: string): string {\n const hex = createHmac(\"sha256\", secret).update(toBuffer(payload)).digest(\"hex\");\n return `sha256=${hex}`;\n}\n\n/**\n * Returns true when `signatureHeader` (the `X-SignSetu-Signature` value) is a\n * valid HMAC-SHA256 of the raw request body under `secret`. Pass the raw body\n * bytes exactly as received; a re-serialized object will not match.\n * Comparison is constant time.\n */\nexport function verifySignature(\n payload: WebhookPayload,\n signatureHeader: string | null | undefined,\n secret: string\n): boolean {\n if (!signatureHeader || !secret) return false;\n const expected = Buffer.from(computeSignature(payload, secret), \"utf8\");\n const received = Buffer.from(signatureHeader.trim(), \"utf8\");\n if (expected.length !== received.length) return false;\n return timingSafeEqual(expected, received);\n}\n\n/**\n * Verifies the signature and returns the parsed event. Throws\n * `WebhookSignatureError` if the signature is missing or invalid.\n */\nexport function constructEvent(\n payload: WebhookPayload,\n signatureHeader: string | null | undefined,\n secret: string\n): WebhookEvent {\n if (!verifySignature(payload, signatureHeader, secret)) {\n throw new WebhookSignatureError();\n }\n return JSON.parse(toBuffer(payload).toString(\"utf8\")) as WebhookEvent;\n}\n\nexport const webhooks = {\n SIGNATURE_HEADER,\n EVENT_HEADER,\n computeSignature,\n verifySignature,\n constructEvent,\n};\n\nexport type Webhooks = typeof webhooks;\n","// Keep in sync with package.json \"version\".\nexport const VERSION = \"0.1.0\";\n","import { SignYuError } from \"./errors\";\nimport { webhooks, type Webhooks } from \"./webhooks\";\nimport type {\n AddSignersResponse,\n CreatedDocument,\n Document,\n DocumentList,\n SendDocumentResponse,\n SignerInput,\n} from \"./types\";\nimport { VERSION } from \"./version\";\n\nexport const DEFAULT_BASE_URL = \"https://signyu.com\";\n\nexport interface SignYuOptions {\n /** Your secret API key, `sk_live_...`. Keep it server side. */\n apiKey: string;\n /** Defaults to `https://signyu.com`. */\n baseUrl?: string;\n /** Per-request timeout in milliseconds. Defaults to 60000. */\n timeoutMs?: number;\n /** Custom fetch implementation (defaults to the global `fetch`). */\n fetch?: typeof fetch;\n}\n\nexport type FileInput = Blob | Uint8Array | ArrayBuffer;\n\nexport interface CreateDocumentParams {\n /** The PDF to sign, at most 10MB. A Node.js `Buffer` works here. */\n file: FileInput;\n /** File name sent with the upload. Defaults to `document.pdf`. */\n fileName?: string;\n /** Label for the document. Defaults to the file name. */\n name?: string;\n}\n\nexport interface ListDocumentsParams {\n /** 1 to 100, default 20. */\n limit?: number;\n /** Default 0. */\n offset?: number;\n}\n\ninterface RequestOptions {\n method: \"GET\" | \"POST\";\n path: string;\n query?: Record<string, string | number | undefined>;\n body?: BodyInit;\n json?: unknown;\n expect?: \"json\" | \"binary\";\n}\n\nfunction toPdfBlob(file: FileInput): Blob {\n if (typeof Blob !== \"undefined\" && file instanceof Blob) {\n // The API rejects anything whose part type is not application/pdf.\n return file.type === \"application/pdf\"\n ? file\n : new Blob([file], { type: \"application/pdf\" });\n }\n if (file instanceof ArrayBuffer) {\n return new Blob([file], { type: \"application/pdf\" });\n }\n if (file instanceof Uint8Array) {\n // Copy into a plain ArrayBuffer-backed view so Blob accepts it everywhere.\n const copy = new Uint8Array(file.byteLength);\n copy.set(file);\n return new Blob([copy], { type: \"application/pdf\" });\n }\n throw new TypeError(\"file must be a Buffer, Uint8Array, ArrayBuffer or Blob.\");\n}\n\nclass Documents {\n constructor(private readonly client: SignYu) {}\n\n /** `POST /api/v1/documents`: upload a PDF and create a `PENDING` document. */\n create(params: CreateDocumentParams): Promise<CreatedDocument> {\n if (!params || params.file === undefined || params.file === null) {\n throw new TypeError(\"documents.create requires a `file`.\");\n }\n const form = new FormData();\n form.append(\"file\", toPdfBlob(params.file), params.fileName ?? \"document.pdf\");\n if (params.name !== undefined) form.append(\"name\", params.name);\n return this.client._request<CreatedDocument>({\n method: \"POST\",\n path: \"/api/v1/documents\",\n body: form,\n });\n }\n\n /** `GET /api/v1/documents`: your documents, most recent first. */\n list(params: ListDocumentsParams = {}): Promise<DocumentList> {\n return this.client._request<DocumentList>({\n method: \"GET\",\n path: \"/api/v1/documents\",\n query: { limit: params.limit, offset: params.offset },\n });\n }\n\n /** `GET /api/v1/documents/{documentId}`: status and per-signer progress. */\n get(documentId: string): Promise<Document> {\n return this.client._request<Document>({\n method: \"GET\",\n path: `/api/v1/documents/${encodeURIComponent(documentId)}`,\n });\n }\n\n /** `POST /api/v1/documents/{documentId}/signers`: add signers while `PENDING` (max 6 per document). */\n addSigners(documentId: string, signers: SignerInput[]): Promise<AddSignersResponse> {\n return this.client._request<AddSignersResponse>({\n method: \"POST\",\n path: `/api/v1/documents/${encodeURIComponent(documentId)}/signers`,\n json: { signers },\n });\n }\n\n /**\n * `POST /api/v1/documents/{documentId}/send`: deducts one credit per signer,\n * emails signing links and returns them. Not idempotent: call it once.\n */\n send(documentId: string): Promise<SendDocumentResponse> {\n return this.client._request<SendDocumentResponse>({\n method: \"POST\",\n path: `/api/v1/documents/${encodeURIComponent(documentId)}/send`,\n });\n }\n\n /**\n * `GET /api/v1/documents/{documentId}/certificate`: the completion\n * certificate and audit trail PDF as raw bytes. Only available once the\n * document is `COMPLETED` (otherwise `409 invalid_state`).\n */\n getCertificate(documentId: string): Promise<Uint8Array> {\n return this.client._request<Uint8Array>({\n method: \"GET\",\n path: `/api/v1/documents/${encodeURIComponent(documentId)}/certificate`,\n expect: \"binary\",\n });\n }\n}\n\nexport class SignYu {\n readonly documents: Documents;\n readonly webhooks: Webhooks = webhooks;\n readonly baseUrl: string;\n\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(options: SignYuOptions) {\n if (!options || !options.apiKey) {\n throw new TypeError(\"SignYu requires an `apiKey`.\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.timeoutMs = options.timeoutMs ?? 60_000;\n const f = options.fetch ?? globalThis.fetch;\n if (typeof f !== \"function\") {\n throw new TypeError(\"No global fetch found. SignYu requires Node.js 18 or newer.\");\n }\n this.fetchImpl = f;\n this.documents = new Documents(this);\n }\n\n /** @internal */\n async _request<T>(opts: RequestOptions): Promise<T> {\n const url = new URL(this.baseUrl + opts.path);\n for (const [k, v] of Object.entries(opts.query ?? {})) {\n if (v !== undefined) url.searchParams.set(k, String(v));\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: opts.expect === \"binary\" ? \"application/pdf\" : \"application/json\",\n \"User-Agent\": `signyu-node/${VERSION}`,\n };\n let body = opts.body;\n if (opts.json !== undefined) {\n headers[\"Content-Type\"] = \"application/json\";\n body = JSON.stringify(opts.json);\n }\n\n let res: Response;\n try {\n res = await this.fetchImpl(url.toString(), {\n method: opts.method,\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n const isTimeout =\n err instanceof Error && (err.name === \"TimeoutError\" || err.name === \"AbortError\");\n throw new SignYuError({\n status: 0,\n code: isTimeout ? \"timeout\" : \"connection_error\",\n message: isTimeout\n ? `Request timed out after ${this.timeoutMs}ms.`\n : `Could not reach the SignYu API: ${err instanceof Error ? err.message : String(err)}`,\n cause: err,\n });\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n let parsed: unknown = undefined;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n parsed = text;\n }\n const errBody =\n parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : {};\n const code =\n typeof errBody.error === \"string\"\n ? errBody.error\n : res.status === 429\n ? \"rate_limited\"\n : \"http_error\";\n const message =\n typeof errBody.message === \"string\"\n ? errBody.message\n : `SignYu API request failed with status ${res.status}.`;\n throw new SignYuError({ status: res.status, code, message, body: parsed });\n }\n\n if (opts.expect === \"binary\") {\n return new Uint8Array(await res.arrayBuffer()) as T;\n }\n return (await res.json()) as T;\n }\n}\n\nexport type { Documents };\n"],"mappings":"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","names":[]}