@assinafy/sdk 2.1.2 → 2.2.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/CHANGELOG.md CHANGED
@@ -7,6 +7,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.0] - 2026-08-27
11
+
12
+ ### Security
13
+
14
+ - **`baseUrl` now requires `https` for any remote host.** Every request on the
15
+ authenticated transport carries the caller's `X-Api-Key` or bearer token, so
16
+ a plaintext `http://` base URL put a long-lived credential on the wire in
17
+ cleartext. `http` remains accepted when the host is loopback (`localhost`,
18
+ the `127.0.0.0/8` block, or `::1`) so local mock servers and the packed
19
+ consumer smoke test keep working; anything else is rejected with a
20
+ `ValidationError` at construction time. `SECURITY.md` already required HTTPS
21
+ endpoints — the client now enforces it rather than trusting configuration.
22
+ - Callers pointing at a remote `http://` host must switch to `https://`.
23
+
24
+ ### Fixed
25
+
26
+ - **`signers.findByEmail()` requested 100 rows per page and received 50.** The
27
+ API clamps `per-page` to 50 and answers `200`, so the lookup scanned half the
28
+ window its documentation claimed. It now pins the real maximum, and the
29
+ method and `signers.list()` documentation state the clamp instead of naming
30
+ 100 as the API maximum. `documents.list()` carried the same incorrect claim.
31
+ - **Unstructured error bodies were reported as the generic "API request
32
+ failed".** A gateway or proxy in front of the API answers with `text/plain`
33
+ or HTML rather than the Assinafy JSON envelope. `ApiError.fromResponse()`
34
+ only read `message`/`error` off an object, so exactly the failures with no
35
+ structured body to explain them lost their only explanation. A non-JSON body
36
+ is now used as the message, with whitespace collapsed and the text capped at
37
+ 500 characters; the untouched body remains on `ApiError.responseData`.
38
+
39
+ ### Added
40
+
41
+ - **`MAX_LIST_PAGE_SIZE`** — the largest page any list endpoint returns (50).
42
+ Exported so callers can size pagination loops against the value the server
43
+ actually honours rather than the one they request.
44
+
45
+ ### Changed
46
+
47
+ - The email and E.164 phone-number patterns had five and two copies across the
48
+ resource files. They are now `isEmail()`, `assertEmail()`, and
49
+ `isE164PhoneNumber()` in `src/utils.ts`, so the accepted shape of a contact
50
+ value is defined in one place. Validation behaviour and error messages are
51
+ unchanged.
52
+ - `decodeBinaryErrorBody()` returns a non-JSON binary body as a string instead
53
+ of wrapping it in `{ message }`, so both unstructured-body paths converge on
54
+ the single rule in `ApiError.fromResponse()`.
55
+ - GitLab CI reads the Bun version from `package.json`'s `packageManager` field
56
+ instead of repeating the literal, matching how GitHub Actions' `setup-bun`
57
+ resolves it. Both CIs now move together when that field is bumped.
58
+ - README gained a contents map linking the setup, end-to-end workflow, and
59
+ per-resource sections; list examples use the `per-page` spelling the API
60
+ reads and document the page-size clamp.
61
+
62
+ ## [2.1.2] - 2026-08-26
63
+
64
+ ### Added
65
+
66
+ - `SDK_USER_AGENT` — every transport, including public and signer-access-code
67
+ requests, now identifies itself as `Assinafy-Typescript-SDK/v<VERSION>`.
68
+ - `scripts/node-consumer-smoke.mjs`, exercised by CI and the release workflow:
69
+ it installs the packed tarball into an isolated consumer and drives the real
70
+ Node HTTP, auth, and multipart paths through both CJS and ESM entrypoints.
71
+ - A manual **Sandbox integration** workflow with `read-only` and
72
+ `disposable-full` modes, gated on a protected environment.
73
+
74
+ ### Security
75
+
76
+ - Requests on the authenticated transport are pinned to the configured API
77
+ origin. A cross-origin URL is rejected before dispatch, and `Authorization`
78
+ and `X-Api-Key` are marked redirect-sensitive so the redirect stack strips
79
+ them.
80
+ - Cross-origin redirects are blocked unless they are a `GET`/`HEAD` to an
81
+ `https` target whose source URL carries no sensitive query parameter.
82
+ - Public, login, and signer-access-code flows run on a transport whose
83
+ credential headers are removed at construction, so a configured API key
84
+ cannot reach a public endpoint.
85
+ - `AxiosError` is no longer attached as an SDK error `cause`. It retains the
86
+ full request config, including credentials and body, which error reporters
87
+ commonly serialize. Transport failures now carry a sanitized cause with
88
+ credential-shaped values redacted from the message.
89
+ - Caller-supplied loggers receive an allowlist of numeric operational counters
90
+ only. Logger exceptions and rejected promises are swallowed so telemetry
91
+ cannot change request semantics.
92
+
93
+ ### Fixed
94
+
95
+ - `AssinafyClient.create()` no longer let its options bag override the
96
+ `apiKey`/`accountId` passed positionally, and it rejects missing or malformed
97
+ credentials instead of constructing an unusable client.
98
+ - Constructor options are validated up front: a non-object bag, a blank
99
+ credential, or an invalid `timeout`/`maxRetries` throws `ValidationError`.
100
+ - `uploadAndRequestSignatures()` validates every signer before uploading, so an
101
+ invalid later signer no longer leaves an orphaned document behind, and it no
102
+ longer waits for `metadata_ready` before creating a virtual assignment.
103
+ - HTTP 429 replay is limited to `GET`, `HEAD`, `OPTIONS`, and `DELETE`, with
104
+ `GET /sign` excluded because that read records a signer view.
105
+
10
106
  ## [2.1.1] - 2026-08-06
11
107
 
12
108
  Maintenance only. The published `dist/` is byte-identical to 2.1.0; no runtime,
@@ -457,7 +553,9 @@ fields (`cpf`, `whatsapp_phone_number`) with the PHP SDK and n8n node.
457
553
  - High-level `uploadAndRequestSignatures` helper on `AssinafyClient`.
458
554
  - `PaginatedResult<T>` with parsed `X-Pagination-*` header meta.
459
555
 
460
- [Unreleased]: https://github.com/assinafy/typescript-sdk/compare/v2.1.1...HEAD
556
+ [Unreleased]: https://github.com/assinafy/typescript-sdk/compare/v2.2.0...HEAD
557
+ [2.2.0]: https://github.com/assinafy/typescript-sdk/compare/v2.1.2...v2.2.0
558
+ [2.1.2]: https://github.com/assinafy/typescript-sdk/compare/v2.1.1...v2.1.2
461
559
  [2.1.1]: https://github.com/assinafy/typescript-sdk/compare/v2.1.0...v2.1.1
462
560
  [2.1.0]: https://github.com/assinafy/typescript-sdk/compare/v2.0.0...v2.1.0
463
561
  [2.0.0]: https://github.com/assinafy/typescript-sdk/releases/tag/v2.0.0
package/README.md CHANGED
@@ -13,6 +13,37 @@ See [API coverage](docs/API_COVERAGE.md) for the operation map and
13
13
  [compatibility notes](docs/COMPATIBILITY.md) for deployment-specific request
14
14
  and response variants.
15
15
 
16
+ ## Contents
17
+
18
+ This document runs from setup to a complete signature workflow, then to
19
+ per-resource detail. Read it in order the first time; use it as a reference
20
+ afterwards.
21
+
22
+ **Getting set up** — [Requirements](#requirements) ·
23
+ [Installation](#installation) · [Quick start](#quick-start) ·
24
+ [Authentication](#authentication) · [Configuration](#configuration)
25
+ ([rate limiting](#rate-limiting), [factories](#factories)) ·
26
+ [Endpoint coverage](#endpoint-coverage)
27
+
28
+ **The end-to-end flow** — [Document lifecycle](#document-lifecycle):
29
+ [upload](#1-upload-the-pdf) → [signers](#2-create-or-reuse-the-email-signers) →
30
+ [price and request signatures](#3-price-then-request-signatures) →
31
+ [the signer's side](#4-complete-the-email-signer-flow) →
32
+ [completion and artifacts](#5-observe-completion-and-download-artifacts)
33
+
34
+ **Per-resource detail** — [Resource reference](#resource-reference):
35
+ [documents](#documents) · [signers](#signers) · [assignments](#assignments) ·
36
+ [paid signing branches](#paid-signing-branches) · [templates](#templates) ·
37
+ [tags](#tags) · [workspaces](#workspaces) ·
38
+ [field definitions](#field-definitions) ·
39
+ [auth and API keys](#authentication--api-key-management) ·
40
+ [the current user](#authenticated-user) · [webhooks](#webhooks)
41
+ ([verification](#webhook-verification)) ·
42
+ [signer-side endpoints](#signer-side-endpoints)
43
+
44
+ **Everything else** — [High-level helper](#high-level-helper) ·
45
+ [Errors](#errors) · [Development](#development) · [License](#license)
46
+
16
47
  ## Requirements
17
48
 
18
49
  - Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads. Packed
@@ -105,7 +136,7 @@ installed package version. The exact value is also exported as
105
136
  | `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
106
137
  | `token` | string | — | Access token (sent as `Authorization: Bearer`). |
107
138
  | `accountId` | string | — | Default workspace/account ID. |
108
- | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Absolute HTTP(S) API base without credentials, query, or fragment. |
139
+ | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Absolute API base without credentials, query, or fragment. Must be `https` unless the host is loopback. |
109
140
  | `webhookSecret` | string | — | Opt-in HMAC secret used by `WebhookVerifier`; see its [contract caveat](docs/COMPATIBILITY.md#webhook-signature-verification-is-not-in-the-openapi-contract). |
110
141
  | `timeout` | number | `30000` | Request timeout in milliseconds. |
111
142
  | `maxRetries` | number | `2` | Auto-retries eligible HTTP 429 responses, honoring `Retry-After`. `0` disables. |
@@ -441,7 +472,7 @@ const doc = await client.documents.upload(
441
472
  await client.documents.upload({ buffer, fileName: 'contract.pdf' });
442
473
 
443
474
  // List → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
444
- const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: 'updated_at' });
475
+ const { data, meta } = await client.documents.list({ page: 1, 'per-page': 20, sort: 'updated_at' });
445
476
 
446
477
  // Search is the lightweight alternative to list: same item shape, but the API
447
478
  // skips the expanded `assignment`/`pages`. Prefer it for name lookups.
@@ -501,7 +532,17 @@ account authentication as their download operations. Prefer
501
532
  credential and returns a `Buffer`. `bundle` contains `original`, `certificated`,
502
533
  and `certificate-page`, plus `pades` when available.
503
534
 
504
- List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pagination-*` headers returned by the API.
535
+ List endpoints return `{ data, meta }`, where `meta` is populated from the
536
+ `X-Pagination-*` response headers. Two API behaviours are worth knowing because
537
+ both are silent rather than errors:
538
+
539
+ - Only the hyphenated `per-page` is read. `per_page` is accepted and ignored,
540
+ falling back to 20 rows — so the SDK rewrites `per_page` to `per-page` on
541
+ every list method, and an explicit `per-page` wins when both are given.
542
+ - `per-page` is clamped to **50**. Asking for 100 returns 50 rows with a `200`.
543
+ The exported `MAX_LIST_PAGE_SIZE` constant is that ceiling; page through with
544
+ `page` rather than requesting a larger page, and trust `meta.per_page` over
545
+ the value you asked for.
505
546
 
506
547
  ### Signers
507
548
 
@@ -525,7 +566,7 @@ await client.signers.create({
525
566
  });
526
567
 
527
568
  await client.signers.get(signerId);
528
- await client.signers.list({ page: 1, per_page: 50, search: 'john' });
569
+ await client.signers.list({ page: 1, 'per-page': 50, search: 'john' });
529
570
  await client.signers.update(signerId, {
530
571
  full_name: 'Johnny Doe',
531
572
  government_id: '390.533.447-05', // official update field; sent as digits
@@ -718,7 +759,7 @@ const created = await client.templates.create(
718
759
  // pages: [], tags: [], created_at: '2026-…', updated_at: '2026-…'
719
760
  // }
720
761
 
721
- const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
762
+ const { data, meta } = await client.templates.list({ search: 'NDA', 'per-page': 20 });
722
763
  const template = await client.templates.get(created.id); // includes pages[] + default_document_tags
723
764
  await client.templates.update(created.id, { name: 'NDA v2', message: 'Please sign' });
724
765
  const firstPage = template.pages?.[0];
@@ -1053,7 +1094,7 @@ await client.signerDocuments.self(accessCode);
1053
1094
  await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
1054
1095
 
1055
1096
  await client.signerDocuments.getCurrent(signerId, accessCode);
1056
- const { data } = await client.signerDocuments.list(signerId, accessCode, { per_page: 20 });
1097
+ const { data } = await client.signerDocuments.list(signerId, accessCode, { 'per-page': 20 });
1057
1098
  // Signer-side counterpart of documents.search(), authorised by the access code.
1058
1099
  const found = await client.signerDocuments.search(signerId, accessCode, 'invoice');
1059
1100
  await client.signerDocuments.download(signerId, documentId, 'original');
package/SECURITY.md CHANGED
@@ -44,8 +44,10 @@ opportunity to upgrade.
44
44
  authentication, and signer-access-code flows use an auth-free transport so
45
45
  configured account credentials are not sent to public endpoints.
46
46
  - Consumers must use HTTPS endpoints and protect all SDK configuration as
47
- secrets. Debug logging and error telemetry must be reviewed for response data
48
- before being enabled in production.
47
+ secrets. The client enforces this: a `baseUrl` using plaintext `http` is
48
+ rejected unless its host is loopback, so a credential cannot be sent in the
49
+ clear to a remote host. Debug logging and error telemetry must be reviewed
50
+ for response data before being enabled in production.
49
51
  - The webhook HMAC helper is an opt-in utility, not proof of an official
50
52
  Assinafy signing contract. Confirm the header, algorithm, encoding, and secret
51
53
  delivery mechanism with Assinafy before enforcing it. See
package/dist/index.d.mts CHANGED
@@ -1261,7 +1261,8 @@ declare class DocumentResource extends BaseResource {
1261
1261
  * @param params - Filters and pagination: `status`; `method` (`virtual` or
1262
1262
  * `collect`); `tags` (comma-separated IDs, all of which must match);
1263
1263
  * `search` (document name, signer name, or signer email); `sort` (`name` or
1264
- * `updated_at`); `page`; and `per-page` (maximum 100).
1264
+ * `updated_at`); `page`; and `per-page` (the server clamps this to 50
1265
+ * rather than rejecting a larger value).
1265
1266
  * @param accountId - Override the client's default account ID.
1266
1267
  * @returns Matching documents, with pagination in `meta`. Each item:
1267
1268
  * ```jsonc
@@ -2099,7 +2100,8 @@ declare class SignerResource extends BaseResource {
2099
2100
  * Pagination info (if any) is attached in `meta`.
2100
2101
  *
2101
2102
  * @param params - `page`, `per-page`, and `search` (matches `full_name` or
2102
- * `email`). The API maximum is 100 items per page.
2103
+ * `email`). The server clamps `per-page` to {@link MAX_LIST_PAGE_SIZE}
2104
+ * (50); a larger value is not rejected, it is silently reduced.
2103
2105
  * @param accountId - Override the client's default account ID.
2104
2106
  * @returns The matching signers, with pagination in `meta`. Each item:
2105
2107
  * ```jsonc
@@ -2181,10 +2183,10 @@ declare class SignerResource extends BaseResource {
2181
2183
  * `search` is a substring match across signer fields, so the result is
2182
2184
  * re-filtered here for an exact, case-insensitive email match.
2183
2185
  *
2184
- * Page size is pinned to the API's maximum of 100.
2185
- * An exact address realistically matches one signer, but a search term that
2186
- * matched more than 100 could in principle miss one — the API exposes no
2187
- * exact-email filter to rule that out.
2186
+ * Page size is pinned to {@link MAX_LIST_PAGE_SIZE}, the largest page the
2187
+ * server actually returns. An exact address realistically matches one
2188
+ * signer, but a search term that matched more than that could in principle
2189
+ * miss one — the API exposes no exact-email filter to rule that out.
2188
2190
  *
2189
2191
  * A `404` from the underlying list is treated as "no match" and mapped to
2190
2192
  * `null`; any other {@link ApiError} propagates.
@@ -5287,9 +5289,14 @@ declare class ApiError extends AssinafyError {
5287
5289
  * Convert a status/body pair into an {@link ApiError}.
5288
5290
  *
5289
5291
  * @param statusCode - Non-success HTTP response status.
5290
- * @param responseData - Parsed API body. String `message` takes priority,
5291
- * followed by string `error`, then the stable fallback message.
5292
- * @returns An `ApiError` retaining the original response body.
5292
+ * @param responseData - API body. For a JSON object, string `message` takes
5293
+ * priority, followed by string `error`. A non-JSON body (a proxy's
5294
+ * `text/plain` or HTML error page) is used verbatim rather than discarded —
5295
+ * otherwise the only failures reported as the generic fallback would be the
5296
+ * ones with no structured body to explain them. Anything else falls back to
5297
+ * the stable message.
5298
+ * @returns An `ApiError` retaining the original response body in
5299
+ * {@link ApiError.responseData}; `message` is truncated for legibility.
5293
5300
  *
5294
5301
  * @example
5295
5302
  * ```ts
@@ -5331,6 +5338,15 @@ declare class NetworkError extends AssinafyError {
5331
5338
  constructor(message: string, options?: ErrorOptions);
5332
5339
  }
5333
5340
 
5341
+ /**
5342
+ * Largest page the list endpoints actually return.
5343
+ *
5344
+ * The API silently clamps `per-page` to this value rather than rejecting a
5345
+ * larger one, so a caller asking for 100 receives 50 and no error. Methods that
5346
+ * need "as many rows as one request can give" pin this instead of guessing.
5347
+ */
5348
+ declare const MAX_LIST_PAGE_SIZE = 50;
5349
+
5334
5350
  declare const SDK_USER_AGENT: string;
5335
5351
 
5336
- export { type AccountLogoUploadSource, type AnyString, ApiError, type AssignmentDisplaySettings, type AssignmentMethod, type AssignmentNotificationMethod, AssignmentResource, type AssignmentVerificationMethod, AssinafyClient, type AssinafyClientOptions, AssinafyError, AuthenticationResource, type ClientConfigInput, DEFAULT_WEBHOOK_EVENTS, type DocumentArtifactName, DocumentResource, type DocumentStatsGranularity, type DocumentStatus, type DocumentUploadSource, FieldsResource, type IAccountTheme, type IApiKeyResponse, type IAssignment, type IAssignmentCostSigner, type IAssignmentEntry, type IAssignmentItem, type IAssignmentListParams, type IAssignmentListResponse, type IAssignmentSigner, type IAuthenticatedUser, type IConfirmSignerDataPayload, type ICostEstimate, type ICreateAssignmentPayload, type ICreateAssignmentResponse, type ICreateDocumentFromTemplateOptions, type ICreateFieldPayload, type ICreateSignerPayload, type ICreateSignerResponse, type ICreateTagPayload, type ICreateWorkspacePayload, type IDeleteTagResponse, type IDetachDocumentTagResponse, type IDisplaySettings, type IDocumentActivity, type IDocumentDetailsResponse, type IDocumentListItem, type IDocumentListParams, type IDocumentListResponse, type IDocumentSearchParams, type IDocumentStatsParams, type IDocumentStatsRow, type IDocumentStatusInfo, type IDocumentUploadOptions, type IDocumentUploadResponse, type IDocumentVerification, type IEstimateAssignmentCostPayload, type IFieldDefinition, type IFieldType, type IFieldValidateMultipleEntry, type IFieldValidationMultipleResult, type IFieldValidationResponse, type IFieldValidationResult, type IInlineTag, type ILegacyConfirmSignerDataPayload, type ILegacyResendCostEstimate, type ILegacyUploadSignatureOptions, type IListParams, type ILoginResponse, type IMaskedApiKeyResponse, type INotificationHistoryEntry, type INotificationPreferences, type IPage, type IPaginatedResponse, type IPublicDocumentInfo, type IRenameDocumentResponse, type IResendCostEstimate, type IResendEmailResponse, type ISignFieldEntry, type ISigner, type ISignerListParams, type ISignerListResponse, type ISignerSelf, type ISigningProgress, type ITag, type ITemplateCostSigner, type ITemplateDetailsResponse, type ITemplateFieldPlacement, type ITemplateListItem, type ITemplateListParams, type ITemplateListResponse, type ITemplateRole, type ITemplateSigner, type IUpdateFieldPayload, type IUpdateNotificationPreferences, type IUpdateSignerPayload, type IUpdateTagPayload, type IUpdateTemplatePayload, type IUpdateWorkspacePayload, type IUploadAndRequestSignaturesResult, type IUploadAndRequestSignaturesSigner, type IUploadSignatureOptions, type IWebhookDispatch, type IWebhookDispatchListParams, type IWebhookEventTypeInfo, type IWebhookPayload, type IWebhookRegisterPayload, type IWebhookSubscription, type IWhatsAppNotification, type IWorkspaceListItem, type IWorkspaceListResponse, type IWorkspaceResponse, type Logger, MAX_UPLOAD_BYTES, NetworkError, type NotificationSenderType, type PaginatedResult, type PaginationMeta, SDK_USER_AGENT, type SendTokenChannel, type SignatureImageType, SignerDocumentsResource, type SignerReference, SignerResource, TagResource, TemplateResource, UserResource, ValidationError, type WebhookEventType, WebhookResource, WebhookVerifier, WorkspaceResource, buildAssignmentPayload };
5352
+ export { type AccountLogoUploadSource, type AnyString, ApiError, type AssignmentDisplaySettings, type AssignmentMethod, type AssignmentNotificationMethod, AssignmentResource, type AssignmentVerificationMethod, AssinafyClient, type AssinafyClientOptions, AssinafyError, AuthenticationResource, type ClientConfigInput, DEFAULT_WEBHOOK_EVENTS, type DocumentArtifactName, DocumentResource, type DocumentStatsGranularity, type DocumentStatus, type DocumentUploadSource, FieldsResource, type IAccountTheme, type IApiKeyResponse, type IAssignment, type IAssignmentCostSigner, type IAssignmentEntry, type IAssignmentItem, type IAssignmentListParams, type IAssignmentListResponse, type IAssignmentSigner, type IAuthenticatedUser, type IConfirmSignerDataPayload, type ICostEstimate, type ICreateAssignmentPayload, type ICreateAssignmentResponse, type ICreateDocumentFromTemplateOptions, type ICreateFieldPayload, type ICreateSignerPayload, type ICreateSignerResponse, type ICreateTagPayload, type ICreateWorkspacePayload, type IDeleteTagResponse, type IDetachDocumentTagResponse, type IDisplaySettings, type IDocumentActivity, type IDocumentDetailsResponse, type IDocumentListItem, type IDocumentListParams, type IDocumentListResponse, type IDocumentSearchParams, type IDocumentStatsParams, type IDocumentStatsRow, type IDocumentStatusInfo, type IDocumentUploadOptions, type IDocumentUploadResponse, type IDocumentVerification, type IEstimateAssignmentCostPayload, type IFieldDefinition, type IFieldType, type IFieldValidateMultipleEntry, type IFieldValidationMultipleResult, type IFieldValidationResponse, type IFieldValidationResult, type IInlineTag, type ILegacyConfirmSignerDataPayload, type ILegacyResendCostEstimate, type ILegacyUploadSignatureOptions, type IListParams, type ILoginResponse, type IMaskedApiKeyResponse, type INotificationHistoryEntry, type INotificationPreferences, type IPage, type IPaginatedResponse, type IPublicDocumentInfo, type IRenameDocumentResponse, type IResendCostEstimate, type IResendEmailResponse, type ISignFieldEntry, type ISigner, type ISignerListParams, type ISignerListResponse, type ISignerSelf, type ISigningProgress, type ITag, type ITemplateCostSigner, type ITemplateDetailsResponse, type ITemplateFieldPlacement, type ITemplateListItem, type ITemplateListParams, type ITemplateListResponse, type ITemplateRole, type ITemplateSigner, type IUpdateFieldPayload, type IUpdateNotificationPreferences, type IUpdateSignerPayload, type IUpdateTagPayload, type IUpdateTemplatePayload, type IUpdateWorkspacePayload, type IUploadAndRequestSignaturesResult, type IUploadAndRequestSignaturesSigner, type IUploadSignatureOptions, type IWebhookDispatch, type IWebhookDispatchListParams, type IWebhookEventTypeInfo, type IWebhookPayload, type IWebhookRegisterPayload, type IWebhookSubscription, type IWhatsAppNotification, type IWorkspaceListItem, type IWorkspaceListResponse, type IWorkspaceResponse, type Logger, MAX_LIST_PAGE_SIZE, MAX_UPLOAD_BYTES, NetworkError, type NotificationSenderType, type PaginatedResult, type PaginationMeta, SDK_USER_AGENT, type SendTokenChannel, type SignatureImageType, SignerDocumentsResource, type SignerReference, SignerResource, TagResource, TemplateResource, UserResource, ValidationError, type WebhookEventType, WebhookResource, WebhookVerifier, WorkspaceResource, buildAssignmentPayload };
package/dist/index.d.ts CHANGED
@@ -1261,7 +1261,8 @@ declare class DocumentResource extends BaseResource {
1261
1261
  * @param params - Filters and pagination: `status`; `method` (`virtual` or
1262
1262
  * `collect`); `tags` (comma-separated IDs, all of which must match);
1263
1263
  * `search` (document name, signer name, or signer email); `sort` (`name` or
1264
- * `updated_at`); `page`; and `per-page` (maximum 100).
1264
+ * `updated_at`); `page`; and `per-page` (the server clamps this to 50
1265
+ * rather than rejecting a larger value).
1265
1266
  * @param accountId - Override the client's default account ID.
1266
1267
  * @returns Matching documents, with pagination in `meta`. Each item:
1267
1268
  * ```jsonc
@@ -2099,7 +2100,8 @@ declare class SignerResource extends BaseResource {
2099
2100
  * Pagination info (if any) is attached in `meta`.
2100
2101
  *
2101
2102
  * @param params - `page`, `per-page`, and `search` (matches `full_name` or
2102
- * `email`). The API maximum is 100 items per page.
2103
+ * `email`). The server clamps `per-page` to {@link MAX_LIST_PAGE_SIZE}
2104
+ * (50); a larger value is not rejected, it is silently reduced.
2103
2105
  * @param accountId - Override the client's default account ID.
2104
2106
  * @returns The matching signers, with pagination in `meta`. Each item:
2105
2107
  * ```jsonc
@@ -2181,10 +2183,10 @@ declare class SignerResource extends BaseResource {
2181
2183
  * `search` is a substring match across signer fields, so the result is
2182
2184
  * re-filtered here for an exact, case-insensitive email match.
2183
2185
  *
2184
- * Page size is pinned to the API's maximum of 100.
2185
- * An exact address realistically matches one signer, but a search term that
2186
- * matched more than 100 could in principle miss one — the API exposes no
2187
- * exact-email filter to rule that out.
2186
+ * Page size is pinned to {@link MAX_LIST_PAGE_SIZE}, the largest page the
2187
+ * server actually returns. An exact address realistically matches one
2188
+ * signer, but a search term that matched more than that could in principle
2189
+ * miss one — the API exposes no exact-email filter to rule that out.
2188
2190
  *
2189
2191
  * A `404` from the underlying list is treated as "no match" and mapped to
2190
2192
  * `null`; any other {@link ApiError} propagates.
@@ -5287,9 +5289,14 @@ declare class ApiError extends AssinafyError {
5287
5289
  * Convert a status/body pair into an {@link ApiError}.
5288
5290
  *
5289
5291
  * @param statusCode - Non-success HTTP response status.
5290
- * @param responseData - Parsed API body. String `message` takes priority,
5291
- * followed by string `error`, then the stable fallback message.
5292
- * @returns An `ApiError` retaining the original response body.
5292
+ * @param responseData - API body. For a JSON object, string `message` takes
5293
+ * priority, followed by string `error`. A non-JSON body (a proxy's
5294
+ * `text/plain` or HTML error page) is used verbatim rather than discarded —
5295
+ * otherwise the only failures reported as the generic fallback would be the
5296
+ * ones with no structured body to explain them. Anything else falls back to
5297
+ * the stable message.
5298
+ * @returns An `ApiError` retaining the original response body in
5299
+ * {@link ApiError.responseData}; `message` is truncated for legibility.
5293
5300
  *
5294
5301
  * @example
5295
5302
  * ```ts
@@ -5331,6 +5338,15 @@ declare class NetworkError extends AssinafyError {
5331
5338
  constructor(message: string, options?: ErrorOptions);
5332
5339
  }
5333
5340
 
5341
+ /**
5342
+ * Largest page the list endpoints actually return.
5343
+ *
5344
+ * The API silently clamps `per-page` to this value rather than rejecting a
5345
+ * larger one, so a caller asking for 100 receives 50 and no error. Methods that
5346
+ * need "as many rows as one request can give" pin this instead of guessing.
5347
+ */
5348
+ declare const MAX_LIST_PAGE_SIZE = 50;
5349
+
5334
5350
  declare const SDK_USER_AGENT: string;
5335
5351
 
5336
- export { type AccountLogoUploadSource, type AnyString, ApiError, type AssignmentDisplaySettings, type AssignmentMethod, type AssignmentNotificationMethod, AssignmentResource, type AssignmentVerificationMethod, AssinafyClient, type AssinafyClientOptions, AssinafyError, AuthenticationResource, type ClientConfigInput, DEFAULT_WEBHOOK_EVENTS, type DocumentArtifactName, DocumentResource, type DocumentStatsGranularity, type DocumentStatus, type DocumentUploadSource, FieldsResource, type IAccountTheme, type IApiKeyResponse, type IAssignment, type IAssignmentCostSigner, type IAssignmentEntry, type IAssignmentItem, type IAssignmentListParams, type IAssignmentListResponse, type IAssignmentSigner, type IAuthenticatedUser, type IConfirmSignerDataPayload, type ICostEstimate, type ICreateAssignmentPayload, type ICreateAssignmentResponse, type ICreateDocumentFromTemplateOptions, type ICreateFieldPayload, type ICreateSignerPayload, type ICreateSignerResponse, type ICreateTagPayload, type ICreateWorkspacePayload, type IDeleteTagResponse, type IDetachDocumentTagResponse, type IDisplaySettings, type IDocumentActivity, type IDocumentDetailsResponse, type IDocumentListItem, type IDocumentListParams, type IDocumentListResponse, type IDocumentSearchParams, type IDocumentStatsParams, type IDocumentStatsRow, type IDocumentStatusInfo, type IDocumentUploadOptions, type IDocumentUploadResponse, type IDocumentVerification, type IEstimateAssignmentCostPayload, type IFieldDefinition, type IFieldType, type IFieldValidateMultipleEntry, type IFieldValidationMultipleResult, type IFieldValidationResponse, type IFieldValidationResult, type IInlineTag, type ILegacyConfirmSignerDataPayload, type ILegacyResendCostEstimate, type ILegacyUploadSignatureOptions, type IListParams, type ILoginResponse, type IMaskedApiKeyResponse, type INotificationHistoryEntry, type INotificationPreferences, type IPage, type IPaginatedResponse, type IPublicDocumentInfo, type IRenameDocumentResponse, type IResendCostEstimate, type IResendEmailResponse, type ISignFieldEntry, type ISigner, type ISignerListParams, type ISignerListResponse, type ISignerSelf, type ISigningProgress, type ITag, type ITemplateCostSigner, type ITemplateDetailsResponse, type ITemplateFieldPlacement, type ITemplateListItem, type ITemplateListParams, type ITemplateListResponse, type ITemplateRole, type ITemplateSigner, type IUpdateFieldPayload, type IUpdateNotificationPreferences, type IUpdateSignerPayload, type IUpdateTagPayload, type IUpdateTemplatePayload, type IUpdateWorkspacePayload, type IUploadAndRequestSignaturesResult, type IUploadAndRequestSignaturesSigner, type IUploadSignatureOptions, type IWebhookDispatch, type IWebhookDispatchListParams, type IWebhookEventTypeInfo, type IWebhookPayload, type IWebhookRegisterPayload, type IWebhookSubscription, type IWhatsAppNotification, type IWorkspaceListItem, type IWorkspaceListResponse, type IWorkspaceResponse, type Logger, MAX_UPLOAD_BYTES, NetworkError, type NotificationSenderType, type PaginatedResult, type PaginationMeta, SDK_USER_AGENT, type SendTokenChannel, type SignatureImageType, SignerDocumentsResource, type SignerReference, SignerResource, TagResource, TemplateResource, UserResource, ValidationError, type WebhookEventType, WebhookResource, WebhookVerifier, WorkspaceResource, buildAssignmentPayload };
5352
+ export { type AccountLogoUploadSource, type AnyString, ApiError, type AssignmentDisplaySettings, type AssignmentMethod, type AssignmentNotificationMethod, AssignmentResource, type AssignmentVerificationMethod, AssinafyClient, type AssinafyClientOptions, AssinafyError, AuthenticationResource, type ClientConfigInput, DEFAULT_WEBHOOK_EVENTS, type DocumentArtifactName, DocumentResource, type DocumentStatsGranularity, type DocumentStatus, type DocumentUploadSource, FieldsResource, type IAccountTheme, type IApiKeyResponse, type IAssignment, type IAssignmentCostSigner, type IAssignmentEntry, type IAssignmentItem, type IAssignmentListParams, type IAssignmentListResponse, type IAssignmentSigner, type IAuthenticatedUser, type IConfirmSignerDataPayload, type ICostEstimate, type ICreateAssignmentPayload, type ICreateAssignmentResponse, type ICreateDocumentFromTemplateOptions, type ICreateFieldPayload, type ICreateSignerPayload, type ICreateSignerResponse, type ICreateTagPayload, type ICreateWorkspacePayload, type IDeleteTagResponse, type IDetachDocumentTagResponse, type IDisplaySettings, type IDocumentActivity, type IDocumentDetailsResponse, type IDocumentListItem, type IDocumentListParams, type IDocumentListResponse, type IDocumentSearchParams, type IDocumentStatsParams, type IDocumentStatsRow, type IDocumentStatusInfo, type IDocumentUploadOptions, type IDocumentUploadResponse, type IDocumentVerification, type IEstimateAssignmentCostPayload, type IFieldDefinition, type IFieldType, type IFieldValidateMultipleEntry, type IFieldValidationMultipleResult, type IFieldValidationResponse, type IFieldValidationResult, type IInlineTag, type ILegacyConfirmSignerDataPayload, type ILegacyResendCostEstimate, type ILegacyUploadSignatureOptions, type IListParams, type ILoginResponse, type IMaskedApiKeyResponse, type INotificationHistoryEntry, type INotificationPreferences, type IPage, type IPaginatedResponse, type IPublicDocumentInfo, type IRenameDocumentResponse, type IResendCostEstimate, type IResendEmailResponse, type ISignFieldEntry, type ISigner, type ISignerListParams, type ISignerListResponse, type ISignerSelf, type ISigningProgress, type ITag, type ITemplateCostSigner, type ITemplateDetailsResponse, type ITemplateFieldPlacement, type ITemplateListItem, type ITemplateListParams, type ITemplateListResponse, type ITemplateRole, type ITemplateSigner, type IUpdateFieldPayload, type IUpdateNotificationPreferences, type IUpdateSignerPayload, type IUpdateTagPayload, type IUpdateTemplatePayload, type IUpdateWorkspacePayload, type IUploadAndRequestSignaturesResult, type IUploadAndRequestSignaturesSigner, type IUploadSignatureOptions, type IWebhookDispatch, type IWebhookDispatchListParams, type IWebhookEventTypeInfo, type IWebhookPayload, type IWebhookRegisterPayload, type IWebhookSubscription, type IWhatsAppNotification, type IWorkspaceListItem, type IWorkspaceListResponse, type IWorkspaceResponse, type Logger, MAX_LIST_PAGE_SIZE, MAX_UPLOAD_BYTES, NetworkError, type NotificationSenderType, type PaginatedResult, type PaginationMeta, SDK_USER_AGENT, type SendTokenChannel, type SignatureImageType, SignerDocumentsResource, type SignerReference, SignerResource, TagResource, TemplateResource, UserResource, ValidationError, type WebhookEventType, WebhookResource, WebhookVerifier, WorkspaceResource, buildAssignmentPayload };
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  DEFAULT_WEBHOOK_EVENTS: () => DEFAULT_WEBHOOK_EVENTS,
39
39
  DocumentResource: () => DocumentResource,
40
40
  FieldsResource: () => FieldsResource,
41
+ MAX_LIST_PAGE_SIZE: () => MAX_LIST_PAGE_SIZE,
41
42
  MAX_UPLOAD_BYTES: () => MAX_UPLOAD_BYTES,
42
43
  NetworkError: () => NetworkError,
43
44
  SDK_USER_AGENT: () => SDK_USER_AGENT,
@@ -59,6 +60,12 @@ var import_axios3 = __toESM(require("axios"));
59
60
  var import_promises = require("timers/promises");
60
61
 
61
62
  // src/errors.ts
63
+ var FALLBACK_MESSAGE = "API request failed";
64
+ var MAX_MESSAGE_LENGTH = 500;
65
+ function summarize(text) {
66
+ const collapsed = text.replaceAll(/\s+/gu, " ");
67
+ return collapsed.length > MAX_MESSAGE_LENGTH ? `${collapsed.slice(0, MAX_MESSAGE_LENGTH)}\u2026` : collapsed;
68
+ }
62
69
  var AssinafyError = class extends Error {
63
70
  context;
64
71
  /**
@@ -100,9 +107,14 @@ var ApiError = class _ApiError extends AssinafyError {
100
107
  * Convert a status/body pair into an {@link ApiError}.
101
108
  *
102
109
  * @param statusCode - Non-success HTTP response status.
103
- * @param responseData - Parsed API body. String `message` takes priority,
104
- * followed by string `error`, then the stable fallback message.
105
- * @returns An `ApiError` retaining the original response body.
110
+ * @param responseData - API body. For a JSON object, string `message` takes
111
+ * priority, followed by string `error`. A non-JSON body (a proxy's
112
+ * `text/plain` or HTML error page) is used verbatim rather than discarded —
113
+ * otherwise the only failures reported as the generic fallback would be the
114
+ * ones with no structured body to explain them. Anything else falls back to
115
+ * the stable message.
116
+ * @returns An `ApiError` retaining the original response body in
117
+ * {@link ApiError.responseData}; `message` is truncated for legibility.
106
118
  *
107
119
  * @example
108
120
  * ```ts
@@ -111,10 +123,14 @@ var ApiError = class _ApiError extends AssinafyError {
111
123
  * ```
112
124
  */
113
125
  static fromResponse(statusCode, responseData) {
126
+ if (typeof responseData === "string") {
127
+ const text = responseData.trim();
128
+ return new _ApiError(text ? summarize(text) : FALLBACK_MESSAGE, statusCode, responseData);
129
+ }
114
130
  const data = responseData ?? {};
115
131
  const rawMessage = data["message"];
116
132
  const rawError = data["error"];
117
- const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : typeof rawError === "string" ? rawError : "API request failed";
133
+ const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : typeof rawError === "string" ? rawError : FALLBACK_MESSAGE;
118
134
  return new _ApiError(message, statusCode, responseData);
119
135
  }
120
136
  };
@@ -189,7 +205,7 @@ function decodeBinaryErrorBody(data) {
189
205
  try {
190
206
  return JSON.parse(text);
191
207
  } catch {
192
- return text.length > 0 ? { message: text } : null;
208
+ return text.length > 0 ? text : null;
193
209
  }
194
210
  }
195
211
  function toSdkError(error, fallbackMessage) {
@@ -270,6 +286,18 @@ function sanitiseNetworkCause(error) {
270
286
  function redactSensitiveErrorText(message) {
271
287
  return message.replace(SENSITIVE_ERROR_VALUE_RE, "$1[REDACTED]").replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gi, "$1[REDACTED]@");
272
288
  }
289
+ function isEmail(value) {
290
+ return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value);
291
+ }
292
+ function assertEmail(value, label = "email") {
293
+ if (!isEmail(value)) {
294
+ throw new ValidationError(`${label} must be a valid email address`, { [label]: value });
295
+ }
296
+ }
297
+ function isE164PhoneNumber(value) {
298
+ return typeof value === "string" && /^\+[1-9]\d{1,14}$/u.test(value);
299
+ }
300
+ var MAX_LIST_PAGE_SIZE = 50;
273
301
  function assertRecord(value, label) {
274
302
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
275
303
  throw new ValidationError(`${label} must be an object`);
@@ -505,7 +533,7 @@ var import_axios2 = __toESM(require("axios"));
505
533
  // package.json
506
534
  var package_default = {
507
535
  name: "@assinafy/sdk",
508
- version: "2.1.2",
536
+ version: "2.2.0",
509
537
  packageManager: "bun@1.4.0",
510
538
  description: "TypeScript SDK for Assinafy API - Digital signature platform",
511
539
  type: "commonjs",
@@ -1554,7 +1582,6 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
1554
1582
  "rejected_by_user",
1555
1583
  "expired"
1556
1584
  ]);
1557
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1558
1585
  var DocumentResource = class extends BaseResource {
1559
1586
  publicHttp;
1560
1587
  constructor(http, defaultAccountId, logger, publicHttp) {
@@ -1639,7 +1666,8 @@ var DocumentResource = class extends BaseResource {
1639
1666
  * @param params - Filters and pagination: `status`; `method` (`virtual` or
1640
1667
  * `collect`); `tags` (comma-separated IDs, all of which must match);
1641
1668
  * `search` (document name, signer name, or signer email); `sort` (`name` or
1642
- * `updated_at`); `page`; and `per-page` (maximum 100).
1669
+ * `updated_at`); `page`; and `per-page` (the server clamps this to 50
1670
+ * rather than rejecting a larger value).
1643
1671
  * @param accountId - Override the client's default account ID.
1644
1672
  * @returns Matching documents, with pagination in `meta`. Each item:
1645
1673
  * ```jsonc
@@ -2542,7 +2570,7 @@ var DocumentResource = class extends BaseResource {
2542
2570
  const id = this.requireId(documentId, "Document ID");
2543
2571
  assertNonEmptyString(recipient, "recipient");
2544
2572
  if (channel !== void 0) assertNonEmptyString(channel, "channel");
2545
- if (channel === void 0 && !EMAIL_RE.test(recipient)) {
2573
+ if (channel === void 0 && !isEmail(recipient)) {
2546
2574
  throw new ValidationError("recipient must be a valid email address");
2547
2575
  }
2548
2576
  const path2 = `/public/documents/${this.pathSegment(id, "Document ID")}/send-token`;
@@ -2729,7 +2757,6 @@ function isLegacySendTokenValidation(error) {
2729
2757
  }
2730
2758
 
2731
2759
  // src/resources/signers.ts
2732
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2733
2760
  var SignerResource = class extends BaseResource {
2734
2761
  /**
2735
2762
  * Create a signer in the workspace (`POST /accounts/{accountId}/signers`).
@@ -2855,7 +2882,8 @@ var SignerResource = class extends BaseResource {
2855
2882
  * Pagination info (if any) is attached in `meta`.
2856
2883
  *
2857
2884
  * @param params - `page`, `per-page`, and `search` (matches `full_name` or
2858
- * `email`). The API maximum is 100 items per page.
2885
+ * `email`). The server clamps `per-page` to {@link MAX_LIST_PAGE_SIZE}
2886
+ * (50); a larger value is not rejected, it is silently reduced.
2859
2887
  * @param accountId - Override the client's default account ID.
2860
2888
  * @returns The matching signers, with pagination in `meta`. Each item:
2861
2889
  * ```jsonc
@@ -2965,10 +2993,10 @@ var SignerResource = class extends BaseResource {
2965
2993
  * `search` is a substring match across signer fields, so the result is
2966
2994
  * re-filtered here for an exact, case-insensitive email match.
2967
2995
  *
2968
- * Page size is pinned to the API's maximum of 100.
2969
- * An exact address realistically matches one signer, but a search term that
2970
- * matched more than 100 could in principle miss one — the API exposes no
2971
- * exact-email filter to rule that out.
2996
+ * Page size is pinned to {@link MAX_LIST_PAGE_SIZE}, the largest page the
2997
+ * server actually returns. An exact address realistically matches one
2998
+ * signer, but a search term that matched more than that could in principle
2999
+ * miss one — the API exposes no exact-email filter to rule that out.
2972
3000
  *
2973
3001
  * A `404` from the underlying list is treated as "no match" and mapped to
2974
3002
  * `null`; any other {@link ApiError} propagates.
@@ -2998,7 +3026,10 @@ var SignerResource = class extends BaseResource {
2998
3026
  async findByEmail(email, accountId) {
2999
3027
  this.assertEmail(email);
3000
3028
  try {
3001
- const { data } = await this.list({ search: email, "per-page": 100 }, accountId);
3029
+ const { data } = await this.list(
3030
+ { search: email, "per-page": MAX_LIST_PAGE_SIZE },
3031
+ accountId
3032
+ );
3002
3033
  const lower = email.toLowerCase();
3003
3034
  return data.find((s) => (s.email ?? "").toLowerCase() === lower) ?? null;
3004
3035
  } catch (err) {
@@ -3009,7 +3040,7 @@ var SignerResource = class extends BaseResource {
3009
3040
  }
3010
3041
  }
3011
3042
  assertEmail(email) {
3012
- if (!email || !EMAIL_RE2.test(email)) {
3043
+ if (!isEmail(email)) {
3013
3044
  throw new ValidationError("Invalid email address", { email });
3014
3045
  }
3015
3046
  }
@@ -3022,7 +3053,7 @@ function validateCreateSignerPayload(payload) {
3022
3053
  throw new ValidationError("full_name is required");
3023
3054
  }
3024
3055
  const phone = payload.whatsapp_phone_number ?? payload.phone;
3025
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE2.test(payload.email))) {
3056
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
3026
3057
  throw new ValidationError("Invalid email address", { email: payload.email });
3027
3058
  }
3028
3059
  validateOptionalPhone(phone);
@@ -3047,7 +3078,7 @@ function validateUpdateSignerPayload(payload) {
3047
3078
  if (payload.full_name !== void 0 && (typeof payload.full_name !== "string" || !payload.full_name.trim())) {
3048
3079
  throw new ValidationError("full_name cannot be empty");
3049
3080
  }
3050
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE2.test(payload.email))) {
3081
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
3051
3082
  throw new ValidationError("Invalid email address", { email: payload.email });
3052
3083
  }
3053
3084
  const phone = payload.whatsapp_phone_number ?? payload.phone;
@@ -3063,7 +3094,7 @@ function validateOptionalDigits(value, field) {
3063
3094
  }
3064
3095
  function validateOptionalPhone(value) {
3065
3096
  if (value === void 0) return;
3066
- if (typeof value !== "string" || !/^\+[1-9]\d{1,14}$/u.test(value)) {
3097
+ if (!isE164PhoneNumber(value)) {
3067
3098
  throw new ValidationError("whatsapp_phone_number must use E.164 format");
3068
3099
  }
3069
3100
  }
@@ -3508,7 +3539,6 @@ var DEFAULT_WEBHOOK_EVENTS = Object.freeze([
3508
3539
  "signer_rejected_document",
3509
3540
  "document_processing_failed"
3510
3541
  ]);
3511
- var EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3512
3542
  var WebhookResource = class extends BaseResource {
3513
3543
  /**
3514
3544
  * Register (or replace) the workspace's single webhook subscription
@@ -3561,7 +3591,7 @@ var WebhookResource = class extends BaseResource {
3561
3591
  throw new ValidationError("Webhook subscription payload is required");
3562
3592
  }
3563
3593
  validateWebhookUrl(payload.url);
3564
- if (!payload.email || !EMAIL_RE3.test(payload.email)) {
3594
+ if (!isEmail(payload.email)) {
3565
3595
  throw new ValidationError("Webhook email must be a valid email address", {
3566
3596
  email: payload.email
3567
3597
  });
@@ -4364,7 +4394,6 @@ function validateTagColor(value) {
4364
4394
  }
4365
4395
 
4366
4396
  // src/resources/authentication.ts
4367
- var EMAIL_RE4 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4368
4397
  var AuthenticationResource = class extends BaseResource {
4369
4398
  publicHttp;
4370
4399
  constructor(http, defaultAccountId, logger, publicHttp) {
@@ -4807,11 +4836,6 @@ var AuthenticationResource = class extends BaseResource {
4807
4836
  return url.toString();
4808
4837
  }
4809
4838
  };
4810
- function assertEmail(value) {
4811
- if (typeof value !== "string" || !EMAIL_RE4.test(value)) {
4812
- throw new ValidationError("email must be a valid email address");
4813
- }
4814
- }
4815
4839
 
4816
4840
  // src/resources/fields.ts
4817
4841
  var FieldsResource = class extends BaseResource {
@@ -5190,8 +5214,6 @@ function validateSignerAccessCode(value) {
5190
5214
  }
5191
5215
 
5192
5216
  // src/resources/signer-documents.ts
5193
- var EMAIL_RE5 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5194
- var E164_RE = /^\+[1-9]\d{1,14}$/u;
5195
5217
  var SignerDocumentsResource = class extends BaseResource {
5196
5218
  constructor(http, defaultAccountId, logger, publicHttp) {
5197
5219
  super(withoutCredentials(publicHttp ?? http), defaultAccountId, logger);
@@ -5848,10 +5870,10 @@ function validateConfirmDataPayload(payload) {
5848
5870
  throw new ValidationError(`${key} must be a string`);
5849
5871
  }
5850
5872
  }
5851
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE5.test(payload.email))) {
5873
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
5852
5874
  throw new ValidationError("email must be a valid email address");
5853
5875
  }
5854
- if (payload.whatsapp_phone_number !== void 0 && (typeof payload.whatsapp_phone_number !== "string" || !E164_RE.test(payload.whatsapp_phone_number))) {
5876
+ if (payload.whatsapp_phone_number !== void 0 && !isE164PhoneNumber(payload.whatsapp_phone_number)) {
5855
5877
  throw new ValidationError("whatsapp_phone_number must use E.164 format");
5856
5878
  }
5857
5879
  if (payload.has_accepted_terms !== void 0 && typeof payload.has_accepted_terms !== "boolean") {
@@ -6617,11 +6639,20 @@ function normaliseBaseUrl(raw) {
6617
6639
  if (url.protocol !== "https:" && url.protocol !== "http:") {
6618
6640
  throw new ValidationError("baseUrl must use http or https");
6619
6641
  }
6642
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
6643
+ throw new ValidationError(
6644
+ "baseUrl must use https for a remote host; http is only allowed for localhost"
6645
+ );
6646
+ }
6620
6647
  if (url.username || url.password || raw.includes("?") || raw.includes("#")) {
6621
6648
  throw new ValidationError("baseUrl must not contain credentials, a query, or a fragment");
6622
6649
  }
6623
6650
  return url.href.replace(/\/+$/, "");
6624
6651
  }
6652
+ function isLoopbackHost(hostname) {
6653
+ const host = hostname.replace(/^\[|\]$/gu, "");
6654
+ return host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(host);
6655
+ }
6625
6656
  function installCredentialOriginGuard(http, baseURL) {
6626
6657
  const allowedOrigin = new URL(baseURL).origin;
6627
6658
  http.interceptors.request.use((config) => {
@@ -6696,6 +6727,7 @@ function hasIdempotencyKey(headers) {
6696
6727
  DEFAULT_WEBHOOK_EVENTS,
6697
6728
  DocumentResource,
6698
6729
  FieldsResource,
6730
+ MAX_LIST_PAGE_SIZE,
6699
6731
  MAX_UPLOAD_BYTES,
6700
6732
  NetworkError,
6701
6733
  SDK_USER_AGENT,
package/dist/index.mjs CHANGED
@@ -3,6 +3,12 @@ import axios3 from "axios";
3
3
  import { setTimeout as delay } from "timers/promises";
4
4
 
5
5
  // src/errors.ts
6
+ var FALLBACK_MESSAGE = "API request failed";
7
+ var MAX_MESSAGE_LENGTH = 500;
8
+ function summarize(text) {
9
+ const collapsed = text.replaceAll(/\s+/gu, " ");
10
+ return collapsed.length > MAX_MESSAGE_LENGTH ? `${collapsed.slice(0, MAX_MESSAGE_LENGTH)}\u2026` : collapsed;
11
+ }
6
12
  var AssinafyError = class extends Error {
7
13
  context;
8
14
  /**
@@ -44,9 +50,14 @@ var ApiError = class _ApiError extends AssinafyError {
44
50
  * Convert a status/body pair into an {@link ApiError}.
45
51
  *
46
52
  * @param statusCode - Non-success HTTP response status.
47
- * @param responseData - Parsed API body. String `message` takes priority,
48
- * followed by string `error`, then the stable fallback message.
49
- * @returns An `ApiError` retaining the original response body.
53
+ * @param responseData - API body. For a JSON object, string `message` takes
54
+ * priority, followed by string `error`. A non-JSON body (a proxy's
55
+ * `text/plain` or HTML error page) is used verbatim rather than discarded —
56
+ * otherwise the only failures reported as the generic fallback would be the
57
+ * ones with no structured body to explain them. Anything else falls back to
58
+ * the stable message.
59
+ * @returns An `ApiError` retaining the original response body in
60
+ * {@link ApiError.responseData}; `message` is truncated for legibility.
50
61
  *
51
62
  * @example
52
63
  * ```ts
@@ -55,10 +66,14 @@ var ApiError = class _ApiError extends AssinafyError {
55
66
  * ```
56
67
  */
57
68
  static fromResponse(statusCode, responseData) {
69
+ if (typeof responseData === "string") {
70
+ const text = responseData.trim();
71
+ return new _ApiError(text ? summarize(text) : FALLBACK_MESSAGE, statusCode, responseData);
72
+ }
58
73
  const data = responseData ?? {};
59
74
  const rawMessage = data["message"];
60
75
  const rawError = data["error"];
61
- const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : typeof rawError === "string" ? rawError : "API request failed";
76
+ const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : typeof rawError === "string" ? rawError : FALLBACK_MESSAGE;
62
77
  return new _ApiError(message, statusCode, responseData);
63
78
  }
64
79
  };
@@ -133,7 +148,7 @@ function decodeBinaryErrorBody(data) {
133
148
  try {
134
149
  return JSON.parse(text);
135
150
  } catch {
136
- return text.length > 0 ? { message: text } : null;
151
+ return text.length > 0 ? text : null;
137
152
  }
138
153
  }
139
154
  function toSdkError(error, fallbackMessage) {
@@ -214,6 +229,18 @@ function sanitiseNetworkCause(error) {
214
229
  function redactSensitiveErrorText(message) {
215
230
  return message.replace(SENSITIVE_ERROR_VALUE_RE, "$1[REDACTED]").replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gi, "$1[REDACTED]@");
216
231
  }
232
+ function isEmail(value) {
233
+ return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value);
234
+ }
235
+ function assertEmail(value, label = "email") {
236
+ if (!isEmail(value)) {
237
+ throw new ValidationError(`${label} must be a valid email address`, { [label]: value });
238
+ }
239
+ }
240
+ function isE164PhoneNumber(value) {
241
+ return typeof value === "string" && /^\+[1-9]\d{1,14}$/u.test(value);
242
+ }
243
+ var MAX_LIST_PAGE_SIZE = 50;
217
244
  function assertRecord(value, label) {
218
245
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
219
246
  throw new ValidationError(`${label} must be an object`);
@@ -449,7 +476,7 @@ import axios2 from "axios";
449
476
  // package.json
450
477
  var package_default = {
451
478
  name: "@assinafy/sdk",
452
- version: "2.1.2",
479
+ version: "2.2.0",
453
480
  packageManager: "bun@1.4.0",
454
481
  description: "TypeScript SDK for Assinafy API - Digital signature platform",
455
482
  type: "commonjs",
@@ -1498,7 +1525,6 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
1498
1525
  "rejected_by_user",
1499
1526
  "expired"
1500
1527
  ]);
1501
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1502
1528
  var DocumentResource = class extends BaseResource {
1503
1529
  publicHttp;
1504
1530
  constructor(http, defaultAccountId, logger, publicHttp) {
@@ -1583,7 +1609,8 @@ var DocumentResource = class extends BaseResource {
1583
1609
  * @param params - Filters and pagination: `status`; `method` (`virtual` or
1584
1610
  * `collect`); `tags` (comma-separated IDs, all of which must match);
1585
1611
  * `search` (document name, signer name, or signer email); `sort` (`name` or
1586
- * `updated_at`); `page`; and `per-page` (maximum 100).
1612
+ * `updated_at`); `page`; and `per-page` (the server clamps this to 50
1613
+ * rather than rejecting a larger value).
1587
1614
  * @param accountId - Override the client's default account ID.
1588
1615
  * @returns Matching documents, with pagination in `meta`. Each item:
1589
1616
  * ```jsonc
@@ -2486,7 +2513,7 @@ var DocumentResource = class extends BaseResource {
2486
2513
  const id = this.requireId(documentId, "Document ID");
2487
2514
  assertNonEmptyString(recipient, "recipient");
2488
2515
  if (channel !== void 0) assertNonEmptyString(channel, "channel");
2489
- if (channel === void 0 && !EMAIL_RE.test(recipient)) {
2516
+ if (channel === void 0 && !isEmail(recipient)) {
2490
2517
  throw new ValidationError("recipient must be a valid email address");
2491
2518
  }
2492
2519
  const path2 = `/public/documents/${this.pathSegment(id, "Document ID")}/send-token`;
@@ -2673,7 +2700,6 @@ function isLegacySendTokenValidation(error) {
2673
2700
  }
2674
2701
 
2675
2702
  // src/resources/signers.ts
2676
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2677
2703
  var SignerResource = class extends BaseResource {
2678
2704
  /**
2679
2705
  * Create a signer in the workspace (`POST /accounts/{accountId}/signers`).
@@ -2799,7 +2825,8 @@ var SignerResource = class extends BaseResource {
2799
2825
  * Pagination info (if any) is attached in `meta`.
2800
2826
  *
2801
2827
  * @param params - `page`, `per-page`, and `search` (matches `full_name` or
2802
- * `email`). The API maximum is 100 items per page.
2828
+ * `email`). The server clamps `per-page` to {@link MAX_LIST_PAGE_SIZE}
2829
+ * (50); a larger value is not rejected, it is silently reduced.
2803
2830
  * @param accountId - Override the client's default account ID.
2804
2831
  * @returns The matching signers, with pagination in `meta`. Each item:
2805
2832
  * ```jsonc
@@ -2909,10 +2936,10 @@ var SignerResource = class extends BaseResource {
2909
2936
  * `search` is a substring match across signer fields, so the result is
2910
2937
  * re-filtered here for an exact, case-insensitive email match.
2911
2938
  *
2912
- * Page size is pinned to the API's maximum of 100.
2913
- * An exact address realistically matches one signer, but a search term that
2914
- * matched more than 100 could in principle miss one — the API exposes no
2915
- * exact-email filter to rule that out.
2939
+ * Page size is pinned to {@link MAX_LIST_PAGE_SIZE}, the largest page the
2940
+ * server actually returns. An exact address realistically matches one
2941
+ * signer, but a search term that matched more than that could in principle
2942
+ * miss one — the API exposes no exact-email filter to rule that out.
2916
2943
  *
2917
2944
  * A `404` from the underlying list is treated as "no match" and mapped to
2918
2945
  * `null`; any other {@link ApiError} propagates.
@@ -2942,7 +2969,10 @@ var SignerResource = class extends BaseResource {
2942
2969
  async findByEmail(email, accountId) {
2943
2970
  this.assertEmail(email);
2944
2971
  try {
2945
- const { data } = await this.list({ search: email, "per-page": 100 }, accountId);
2972
+ const { data } = await this.list(
2973
+ { search: email, "per-page": MAX_LIST_PAGE_SIZE },
2974
+ accountId
2975
+ );
2946
2976
  const lower = email.toLowerCase();
2947
2977
  return data.find((s) => (s.email ?? "").toLowerCase() === lower) ?? null;
2948
2978
  } catch (err) {
@@ -2953,7 +2983,7 @@ var SignerResource = class extends BaseResource {
2953
2983
  }
2954
2984
  }
2955
2985
  assertEmail(email) {
2956
- if (!email || !EMAIL_RE2.test(email)) {
2986
+ if (!isEmail(email)) {
2957
2987
  throw new ValidationError("Invalid email address", { email });
2958
2988
  }
2959
2989
  }
@@ -2966,7 +2996,7 @@ function validateCreateSignerPayload(payload) {
2966
2996
  throw new ValidationError("full_name is required");
2967
2997
  }
2968
2998
  const phone = payload.whatsapp_phone_number ?? payload.phone;
2969
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE2.test(payload.email))) {
2999
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
2970
3000
  throw new ValidationError("Invalid email address", { email: payload.email });
2971
3001
  }
2972
3002
  validateOptionalPhone(phone);
@@ -2991,7 +3021,7 @@ function validateUpdateSignerPayload(payload) {
2991
3021
  if (payload.full_name !== void 0 && (typeof payload.full_name !== "string" || !payload.full_name.trim())) {
2992
3022
  throw new ValidationError("full_name cannot be empty");
2993
3023
  }
2994
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE2.test(payload.email))) {
3024
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
2995
3025
  throw new ValidationError("Invalid email address", { email: payload.email });
2996
3026
  }
2997
3027
  const phone = payload.whatsapp_phone_number ?? payload.phone;
@@ -3007,7 +3037,7 @@ function validateOptionalDigits(value, field) {
3007
3037
  }
3008
3038
  function validateOptionalPhone(value) {
3009
3039
  if (value === void 0) return;
3010
- if (typeof value !== "string" || !/^\+[1-9]\d{1,14}$/u.test(value)) {
3040
+ if (!isE164PhoneNumber(value)) {
3011
3041
  throw new ValidationError("whatsapp_phone_number must use E.164 format");
3012
3042
  }
3013
3043
  }
@@ -3452,7 +3482,6 @@ var DEFAULT_WEBHOOK_EVENTS = Object.freeze([
3452
3482
  "signer_rejected_document",
3453
3483
  "document_processing_failed"
3454
3484
  ]);
3455
- var EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3456
3485
  var WebhookResource = class extends BaseResource {
3457
3486
  /**
3458
3487
  * Register (or replace) the workspace's single webhook subscription
@@ -3505,7 +3534,7 @@ var WebhookResource = class extends BaseResource {
3505
3534
  throw new ValidationError("Webhook subscription payload is required");
3506
3535
  }
3507
3536
  validateWebhookUrl(payload.url);
3508
- if (!payload.email || !EMAIL_RE3.test(payload.email)) {
3537
+ if (!isEmail(payload.email)) {
3509
3538
  throw new ValidationError("Webhook email must be a valid email address", {
3510
3539
  email: payload.email
3511
3540
  });
@@ -4308,7 +4337,6 @@ function validateTagColor(value) {
4308
4337
  }
4309
4338
 
4310
4339
  // src/resources/authentication.ts
4311
- var EMAIL_RE4 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4312
4340
  var AuthenticationResource = class extends BaseResource {
4313
4341
  publicHttp;
4314
4342
  constructor(http, defaultAccountId, logger, publicHttp) {
@@ -4751,11 +4779,6 @@ var AuthenticationResource = class extends BaseResource {
4751
4779
  return url.toString();
4752
4780
  }
4753
4781
  };
4754
- function assertEmail(value) {
4755
- if (typeof value !== "string" || !EMAIL_RE4.test(value)) {
4756
- throw new ValidationError("email must be a valid email address");
4757
- }
4758
- }
4759
4782
 
4760
4783
  // src/resources/fields.ts
4761
4784
  var FieldsResource = class extends BaseResource {
@@ -5134,8 +5157,6 @@ function validateSignerAccessCode(value) {
5134
5157
  }
5135
5158
 
5136
5159
  // src/resources/signer-documents.ts
5137
- var EMAIL_RE5 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5138
- var E164_RE = /^\+[1-9]\d{1,14}$/u;
5139
5160
  var SignerDocumentsResource = class extends BaseResource {
5140
5161
  constructor(http, defaultAccountId, logger, publicHttp) {
5141
5162
  super(withoutCredentials(publicHttp ?? http), defaultAccountId, logger);
@@ -5792,10 +5813,10 @@ function validateConfirmDataPayload(payload) {
5792
5813
  throw new ValidationError(`${key} must be a string`);
5793
5814
  }
5794
5815
  }
5795
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE5.test(payload.email))) {
5816
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
5796
5817
  throw new ValidationError("email must be a valid email address");
5797
5818
  }
5798
- if (payload.whatsapp_phone_number !== void 0 && (typeof payload.whatsapp_phone_number !== "string" || !E164_RE.test(payload.whatsapp_phone_number))) {
5819
+ if (payload.whatsapp_phone_number !== void 0 && !isE164PhoneNumber(payload.whatsapp_phone_number)) {
5799
5820
  throw new ValidationError("whatsapp_phone_number must use E.164 format");
5800
5821
  }
5801
5822
  if (payload.has_accepted_terms !== void 0 && typeof payload.has_accepted_terms !== "boolean") {
@@ -6561,11 +6582,20 @@ function normaliseBaseUrl(raw) {
6561
6582
  if (url.protocol !== "https:" && url.protocol !== "http:") {
6562
6583
  throw new ValidationError("baseUrl must use http or https");
6563
6584
  }
6585
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
6586
+ throw new ValidationError(
6587
+ "baseUrl must use https for a remote host; http is only allowed for localhost"
6588
+ );
6589
+ }
6564
6590
  if (url.username || url.password || raw.includes("?") || raw.includes("#")) {
6565
6591
  throw new ValidationError("baseUrl must not contain credentials, a query, or a fragment");
6566
6592
  }
6567
6593
  return url.href.replace(/\/+$/, "");
6568
6594
  }
6595
+ function isLoopbackHost(hostname) {
6596
+ const host = hostname.replace(/^\[|\]$/gu, "");
6597
+ return host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(host);
6598
+ }
6569
6599
  function installCredentialOriginGuard(http, baseURL) {
6570
6600
  const allowedOrigin = new URL(baseURL).origin;
6571
6601
  http.interceptors.request.use((config) => {
@@ -6639,6 +6669,7 @@ export {
6639
6669
  DEFAULT_WEBHOOK_EVENTS,
6640
6670
  DocumentResource,
6641
6671
  FieldsResource,
6672
+ MAX_LIST_PAGE_SIZE,
6642
6673
  MAX_UPLOAD_BYTES,
6643
6674
  NetworkError,
6644
6675
  SDK_USER_AGENT,
@@ -58,6 +58,29 @@ method counters. Notification counters are not mutually exclusive.
58
58
  are mutually exclusive and sum to `signature_requests`. Older unsuffixed email
59
59
  and WhatsApp counters remain optional.
60
60
 
61
+ ## List pagination
62
+
63
+ Two behaviours of the list endpoints are silent rather than loud, so neither
64
+ surfaces as an error a caller could react to:
65
+
66
+ - Only the hyphenated `per-page` is read. `per_page` is accepted and discarded,
67
+ and the response falls back to the default of 20 rows. The SDK normalizes
68
+ `per_page` to `per-page` on every list method, and an explicit `per-page`
69
+ wins if both are supplied.
70
+ - `per-page` is clamped to **50**. A request for 100 returns 50 rows with a
71
+ `200`, so page-size arithmetic based on the requested value is wrong by half.
72
+ That maximum is exported as `MAX_LIST_PAGE_SIZE`.
73
+
74
+ ```ts
75
+ import { MAX_LIST_PAGE_SIZE } from '@assinafy/sdk';
76
+
77
+ const { data, meta } = await client.documents.list({ 'per-page': MAX_LIST_PAGE_SIZE });
78
+ // meta is read from the X-Pagination-* response headers, so it reports the
79
+ // page size the server actually applied — prefer it over the requested value.
80
+ ```
81
+
82
+ `signers.findByEmail()` pins this maximum for its single lookup request.
83
+
61
84
  ## Assignment list account context
62
85
 
63
86
  `GET /v1/assignments` requires the workspace in the camel-case `accountId`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assinafy/sdk",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "packageManager": "bun@1.4.0",
5
5
  "description": "TypeScript SDK for Assinafy API - Digital signature platform",
6
6
  "type": "commonjs",