@assinafy/sdk 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,8 +38,11 @@ __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,
44
+ OAuthError: () => OAuthError,
45
+ OAuthResource: () => OAuthResource,
43
46
  SDK_USER_AGENT: () => SDK_USER_AGENT,
44
47
  SignerDocumentsResource: () => SignerDocumentsResource,
45
48
  SignerResource: () => SignerResource,
@@ -50,7 +53,8 @@ __export(index_exports, {
50
53
  WebhookResource: () => WebhookResource,
51
54
  WebhookVerifier: () => WebhookVerifier,
52
55
  WorkspaceResource: () => WorkspaceResource,
53
- buildAssignmentPayload: () => buildAssignmentPayload
56
+ buildAssignmentPayload: () => buildAssignmentPayload,
57
+ parseWwwAuthenticate: () => parseWwwAuthenticate
54
58
  });
55
59
  module.exports = __toCommonJS(index_exports);
56
60
 
@@ -59,6 +63,12 @@ var import_axios3 = __toESM(require("axios"));
59
63
  var import_promises = require("timers/promises");
60
64
 
61
65
  // src/errors.ts
66
+ var FALLBACK_MESSAGE = "API request failed";
67
+ var MAX_MESSAGE_LENGTH = 500;
68
+ function summarize(text) {
69
+ const collapsed = text.replaceAll(/\s+/gu, " ");
70
+ return collapsed.length > MAX_MESSAGE_LENGTH ? `${collapsed.slice(0, MAX_MESSAGE_LENGTH)}\u2026` : collapsed;
71
+ }
62
72
  var AssinafyError = class extends Error {
63
73
  context;
64
74
  /**
@@ -82,6 +92,29 @@ var AssinafyError = class extends Error {
82
92
  var ApiError = class _ApiError extends AssinafyError {
83
93
  statusCode;
84
94
  responseData;
95
+ /**
96
+ * Parsed `WWW-Authenticate` challenge, when the response carried one.
97
+ *
98
+ * A `403` whose challenge is `{ error: 'insufficient_scope', scope: '…' }`
99
+ * means the OAuth token is valid but was never granted that permission:
100
+ * send the user through the authorization flow again asking for the scope
101
+ * named in `scope`. A `403` without a challenge has a different cause —
102
+ * another workspace, the user's role, or a surface OAuth tokens never
103
+ * reach — and reconnecting will not fix it.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * try {
108
+ * await connected.documents.upload({ filePath: './contract.pdf' });
109
+ * } catch (error) {
110
+ * if (error instanceof ApiError && error.challenge?.error === 'insufficient_scope') {
111
+ * return reconnect(error.challenge.scope); // 'documents:write'
112
+ * }
113
+ * throw error;
114
+ * }
115
+ * ```
116
+ */
117
+ challenge;
85
118
  /**
86
119
  * Create an error representing a non-success API response.
87
120
  *
@@ -100,9 +133,14 @@ var ApiError = class _ApiError extends AssinafyError {
100
133
  * Convert a status/body pair into an {@link ApiError}.
101
134
  *
102
135
  * @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.
136
+ * @param responseData - API body. For a JSON object, string `message` takes
137
+ * priority, followed by string `error`. A non-JSON body (a proxy's
138
+ * `text/plain` or HTML error page) is used verbatim rather than discarded —
139
+ * otherwise the only failures reported as the generic fallback would be the
140
+ * ones with no structured body to explain them. Anything else falls back to
141
+ * the stable message.
142
+ * @returns An `ApiError` retaining the original response body in
143
+ * {@link ApiError.responseData}; `message` is truncated for legibility.
106
144
  *
107
145
  * @example
108
146
  * ```ts
@@ -111,13 +149,66 @@ var ApiError = class _ApiError extends AssinafyError {
111
149
  * ```
112
150
  */
113
151
  static fromResponse(statusCode, responseData) {
152
+ if (typeof responseData === "string") {
153
+ const text = responseData.trim();
154
+ return new _ApiError(text ? summarize(text) : FALLBACK_MESSAGE, statusCode, responseData);
155
+ }
114
156
  const data = responseData ?? {};
115
157
  const rawMessage = data["message"];
116
158
  const rawError = data["error"];
117
- const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : typeof rawError === "string" ? rawError : "API request failed";
159
+ const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : typeof rawError === "string" ? rawError : FALLBACK_MESSAGE;
118
160
  return new _ApiError(message, statusCode, responseData);
119
161
  }
120
162
  };
163
+ var OAuthError = class _OAuthError extends ApiError {
164
+ /** RFC 6749 error code, e.g. `invalid_grant`. */
165
+ error;
166
+ /** The server's human-readable explanation, when it sent one. */
167
+ errorDescription;
168
+ /**
169
+ * Create an OAuth protocol error.
170
+ *
171
+ * @param error - RFC 6749 error code.
172
+ * @param errorDescription - Server-provided explanation, or `null`.
173
+ * @param statusCode - HTTP status that carried it. Authorization responses
174
+ * arrive as redirect query parameters rather than an HTTP response, so
175
+ * {@link OAuthResource.readAuthorizationCallback} reports them as `400`.
176
+ * @param responseData - The raw error object.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * throw new OAuthError('invalid_grant', 'Authorization code expired.', 400);
181
+ * ```
182
+ */
183
+ constructor(error, errorDescription = null, statusCode = 400, responseData = null) {
184
+ super(errorDescription ? `${error}: ${errorDescription}` : error, statusCode, responseData);
185
+ this.name = "OAuthError";
186
+ this.error = error;
187
+ this.errorDescription = errorDescription;
188
+ }
189
+ /**
190
+ * Upgrade an {@link ApiError} to an {@link OAuthError} when its body is an
191
+ * RFC 6749 error object; otherwise return the value untouched.
192
+ *
193
+ * @param error - Any thrown value.
194
+ * @returns An `OAuthError` when the body carries a non-empty string
195
+ * `error`, else the original value.
196
+ */
197
+ static upgrade(error) {
198
+ if (!(error instanceof ApiError) || error instanceof _OAuthError) return error;
199
+ const body = error.responseData;
200
+ if (body === null || typeof body !== "object") return error;
201
+ const code = body["error"];
202
+ if (typeof code !== "string" || code.length === 0) return error;
203
+ const description = body["error_description"];
204
+ return new _OAuthError(
205
+ code,
206
+ typeof description === "string" && description.length > 0 ? description : null,
207
+ error.statusCode,
208
+ body
209
+ );
210
+ }
211
+ };
121
212
  var ValidationError = class extends AssinafyError {
122
213
  errors;
123
214
  /**
@@ -157,6 +248,43 @@ var NetworkError = class extends AssinafyError {
157
248
 
158
249
  // src/utils.ts
159
250
  var import_axios = __toESM(require("axios"));
251
+
252
+ // src/support/headers.ts
253
+ function readHeader(headers, name) {
254
+ if (!headers) return void 0;
255
+ const lower = name.toLowerCase();
256
+ for (const [key, value] of Object.entries(headers)) {
257
+ if (key.toLowerCase() === lower && value != null) {
258
+ const first = Array.isArray(value) ? value[0] : value;
259
+ if (typeof first === "string" || typeof first === "number" || typeof first === "boolean") {
260
+ return String(first);
261
+ }
262
+ return void 0;
263
+ }
264
+ }
265
+ return void 0;
266
+ }
267
+ var CHALLENGE_PARAM = /([A-Za-z0-9_-]+)\s*=\s*(?:"([^"]*)"|([^\s,]+))/gu;
268
+ function parseWwwAuthenticate(value) {
269
+ if (typeof value !== "string") return void 0;
270
+ const trimmed = value.trim();
271
+ const scheme = /^[A-Za-z0-9_-]+/u.exec(trimmed)?.[0];
272
+ if (!scheme) return void 0;
273
+ const challenge = { scheme };
274
+ CHALLENGE_PARAM.lastIndex = 0;
275
+ for (const match of trimmed.slice(scheme.length).matchAll(CHALLENGE_PARAM)) {
276
+ const key = match[1]?.toLowerCase();
277
+ const paramValue = match[2] ?? match[3];
278
+ if (paramValue === void 0) continue;
279
+ if (key === "error") challenge.error = paramValue;
280
+ else if (key === "error_description") challenge.error_description = paramValue;
281
+ else if (key === "scope") challenge.scope = paramValue;
282
+ else if (key === "resource_metadata") challenge.resource_metadata = paramValue;
283
+ }
284
+ return challenge;
285
+ }
286
+
287
+ // src/utils.ts
160
288
  var SAFE_LOG_NUMBER_FIELDS = /* @__PURE__ */ new Set([
161
289
  "attempt",
162
290
  "attempts",
@@ -189,7 +317,7 @@ function decodeBinaryErrorBody(data) {
189
317
  try {
190
318
  return JSON.parse(text);
191
319
  } catch {
192
- return text.length > 0 ? { message: text } : null;
320
+ return text.length > 0 ? text : null;
193
321
  }
194
322
  }
195
323
  function toSdkError(error, fallbackMessage) {
@@ -200,7 +328,12 @@ function toSdkError(error, fallbackMessage) {
200
328
  const status = error.response?.status;
201
329
  if (status) {
202
330
  const body = decodeBinaryErrorBody(error.response?.data ?? null);
203
- return ApiError.fromResponse(status, body ?? null);
331
+ const apiError = ApiError.fromResponse(status, body ?? null);
332
+ const challenge = parseWwwAuthenticate(
333
+ readHeader(error.response?.headers, "www-authenticate")
334
+ );
335
+ if (challenge) apiError.challenge = challenge;
336
+ return apiError;
204
337
  }
205
338
  const cause = sanitiseNetworkCause(error);
206
339
  return new NetworkError(`${fallbackMessage}: ${cause.message}`, { cause });
@@ -270,6 +403,18 @@ function sanitiseNetworkCause(error) {
270
403
  function redactSensitiveErrorText(message) {
271
404
  return message.replace(SENSITIVE_ERROR_VALUE_RE, "$1[REDACTED]").replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gi, "$1[REDACTED]@");
272
405
  }
406
+ function isEmail(value) {
407
+ return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value);
408
+ }
409
+ function assertEmail(value, label = "email") {
410
+ if (!isEmail(value)) {
411
+ throw new ValidationError(`${label} must be a valid email address`, { [label]: value });
412
+ }
413
+ }
414
+ function isE164PhoneNumber(value) {
415
+ return typeof value === "string" && /^\+[1-9]\d{1,14}$/u.test(value);
416
+ }
417
+ var MAX_LIST_PAGE_SIZE = 50;
273
418
  function assertRecord(value, label) {
274
419
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
275
420
  throw new ValidationError(`${label} must be an object`);
@@ -338,22 +483,6 @@ function cleanListParams(params) {
338
483
  return out;
339
484
  }
340
485
 
341
- // src/support/headers.ts
342
- function readHeader(headers, name) {
343
- if (!headers) return void 0;
344
- const lower = name.toLowerCase();
345
- for (const [key, value] of Object.entries(headers)) {
346
- if (key.toLowerCase() === lower && value != null) {
347
- const first = Array.isArray(value) ? value[0] : value;
348
- if (typeof first === "string" || typeof first === "number" || typeof first === "boolean") {
349
- return String(first);
350
- }
351
- return void 0;
352
- }
353
- }
354
- return void 0;
355
- }
356
-
357
486
  // src/support/retry.ts
358
487
  function retryDelayFromHeaders(headers) {
359
488
  const retryAfter = readHeader(headers, "retry-after");
@@ -505,7 +634,7 @@ var import_axios2 = __toESM(require("axios"));
505
634
  // package.json
506
635
  var package_default = {
507
636
  name: "@assinafy/sdk",
508
- version: "2.1.2",
637
+ version: "2.3.0",
509
638
  packageManager: "bun@1.4.0",
510
639
  description: "TypeScript SDK for Assinafy API - Digital signature platform",
511
640
  type: "commonjs",
@@ -592,6 +721,7 @@ var package_default = {
592
721
  "dist",
593
722
  "docs",
594
723
  "README.md",
724
+ "README.en.md",
595
725
  "CHANGELOG.md",
596
726
  "SECURITY.md",
597
727
  "LICENSE"
@@ -843,14 +973,34 @@ function toInt(value) {
843
973
  var ASSIGNMENT_METHODS = /* @__PURE__ */ new Set(["virtual", "collect"]);
844
974
  var VERIFICATION_METHODS = /* @__PURE__ */ new Set(["Email", "Whatsapp", "DigitalCertificate"]);
845
975
  var NOTIFICATION_METHODS = /* @__PURE__ */ new Set(["Email", "Whatsapp"]);
976
+ var ALLOWED_NOTIFICATION_METHODS = {
977
+ Email: /* @__PURE__ */ new Set(["Email"]),
978
+ Whatsapp: /* @__PURE__ */ new Set(["Whatsapp"]),
979
+ DigitalCertificate: NOTIFICATION_METHODS
980
+ };
846
981
  function validateAssignmentSignerOptions(signer, label = "signer") {
847
982
  if (signer.verification_method !== void 0 && (typeof signer.verification_method !== "string" || !VERIFICATION_METHODS.has(signer.verification_method))) {
848
983
  throw new ValidationError(`${label} has an invalid verification_method`);
849
984
  }
850
- if (signer.notification_methods !== void 0 && (!Array.isArray(signer.notification_methods) || signer.notification_methods.some(
851
- (method) => typeof method !== "string" || !NOTIFICATION_METHODS.has(method)
852
- ))) {
853
- throw new ValidationError(`${label} has invalid notification_methods`);
985
+ if (signer.notification_methods !== void 0) {
986
+ const methods = signer.notification_methods;
987
+ if (!Array.isArray(methods) || methods.some(
988
+ (method) => typeof method !== "string" || !NOTIFICATION_METHODS.has(method)
989
+ )) {
990
+ throw new ValidationError(`${label} has invalid notification_methods`);
991
+ }
992
+ if (methods.length !== 1) {
993
+ throw new ValidationError(`${label} allows exactly one notification method`);
994
+ }
995
+ const verification = signer.verification_method;
996
+ if (typeof verification === "string") {
997
+ const allowed = ALLOWED_NOTIFICATION_METHODS[verification];
998
+ if (allowed && !allowed.has(methods[0])) {
999
+ throw new ValidationError(
1000
+ `${label} cannot pair ${verification} verification with ${String(methods[0])} notification`
1001
+ );
1002
+ }
1003
+ }
854
1004
  }
855
1005
  if (signer.step !== void 0 && (typeof signer.step !== "number" || !Number.isSafeInteger(signer.step) || signer.step < 1)) {
856
1006
  throw new ValidationError(`${label} step must be a positive safe integer`);
@@ -1554,7 +1704,6 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
1554
1704
  "rejected_by_user",
1555
1705
  "expired"
1556
1706
  ]);
1557
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1558
1707
  var DocumentResource = class extends BaseResource {
1559
1708
  publicHttp;
1560
1709
  constructor(http, defaultAccountId, logger, publicHttp) {
@@ -1639,7 +1788,8 @@ var DocumentResource = class extends BaseResource {
1639
1788
  * @param params - Filters and pagination: `status`; `method` (`virtual` or
1640
1789
  * `collect`); `tags` (comma-separated IDs, all of which must match);
1641
1790
  * `search` (document name, signer name, or signer email); `sort` (`name` or
1642
- * `updated_at`); `page`; and `per-page` (maximum 100).
1791
+ * `updated_at`); `page`; and `per-page` (the server clamps this to 50
1792
+ * rather than rejecting a larger value).
1643
1793
  * @param accountId - Override the client's default account ID.
1644
1794
  * @returns Matching documents, with pagination in `meta`. Each item:
1645
1795
  * ```jsonc
@@ -2542,7 +2692,7 @@ var DocumentResource = class extends BaseResource {
2542
2692
  const id = this.requireId(documentId, "Document ID");
2543
2693
  assertNonEmptyString(recipient, "recipient");
2544
2694
  if (channel !== void 0) assertNonEmptyString(channel, "channel");
2545
- if (channel === void 0 && !EMAIL_RE.test(recipient)) {
2695
+ if (channel === void 0 && !isEmail(recipient)) {
2546
2696
  throw new ValidationError("recipient must be a valid email address");
2547
2697
  }
2548
2698
  const path2 = `/public/documents/${this.pathSegment(id, "Document ID")}/send-token`;
@@ -2644,9 +2794,6 @@ function normaliseTemplateSigners(signers) {
2644
2794
  throw new ValidationError(`Template signer ${index + 1} requires id`);
2645
2795
  }
2646
2796
  validateAssignmentSignerOptions(signer, `Template signer ${index + 1}`);
2647
- if (signer.notification_methods !== void 0 && signer.notification_methods.length > 1) {
2648
- throw new ValidationError(`Template signer ${index + 1} allows one notification method`);
2649
- }
2650
2797
  const projected = { role_id: signer.role_id, id: signer.id };
2651
2798
  if (signer.verification_method !== void 0) {
2652
2799
  projected.verification_method = signer.verification_method;
@@ -2729,7 +2876,6 @@ function isLegacySendTokenValidation(error) {
2729
2876
  }
2730
2877
 
2731
2878
  // src/resources/signers.ts
2732
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2733
2879
  var SignerResource = class extends BaseResource {
2734
2880
  /**
2735
2881
  * Create a signer in the workspace (`POST /accounts/{accountId}/signers`).
@@ -2855,7 +3001,8 @@ var SignerResource = class extends BaseResource {
2855
3001
  * Pagination info (if any) is attached in `meta`.
2856
3002
  *
2857
3003
  * @param params - `page`, `per-page`, and `search` (matches `full_name` or
2858
- * `email`). The API maximum is 100 items per page.
3004
+ * `email`). The server clamps `per-page` to {@link MAX_LIST_PAGE_SIZE}
3005
+ * (50); a larger value is not rejected, it is silently reduced.
2859
3006
  * @param accountId - Override the client's default account ID.
2860
3007
  * @returns The matching signers, with pagination in `meta`. Each item:
2861
3008
  * ```jsonc
@@ -2965,10 +3112,10 @@ var SignerResource = class extends BaseResource {
2965
3112
  * `search` is a substring match across signer fields, so the result is
2966
3113
  * re-filtered here for an exact, case-insensitive email match.
2967
3114
  *
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.
3115
+ * Page size is pinned to {@link MAX_LIST_PAGE_SIZE}, the largest page the
3116
+ * server actually returns. An exact address realistically matches one
3117
+ * signer, but a search term that matched more than that could in principle
3118
+ * miss one — the API exposes no exact-email filter to rule that out.
2972
3119
  *
2973
3120
  * A `404` from the underlying list is treated as "no match" and mapped to
2974
3121
  * `null`; any other {@link ApiError} propagates.
@@ -2998,7 +3145,10 @@ var SignerResource = class extends BaseResource {
2998
3145
  async findByEmail(email, accountId) {
2999
3146
  this.assertEmail(email);
3000
3147
  try {
3001
- const { data } = await this.list({ search: email, "per-page": 100 }, accountId);
3148
+ const { data } = await this.list(
3149
+ { search: email, "per-page": MAX_LIST_PAGE_SIZE },
3150
+ accountId
3151
+ );
3002
3152
  const lower = email.toLowerCase();
3003
3153
  return data.find((s) => (s.email ?? "").toLowerCase() === lower) ?? null;
3004
3154
  } catch (err) {
@@ -3009,7 +3159,7 @@ var SignerResource = class extends BaseResource {
3009
3159
  }
3010
3160
  }
3011
3161
  assertEmail(email) {
3012
- if (!email || !EMAIL_RE2.test(email)) {
3162
+ if (!isEmail(email)) {
3013
3163
  throw new ValidationError("Invalid email address", { email });
3014
3164
  }
3015
3165
  }
@@ -3022,7 +3172,7 @@ function validateCreateSignerPayload(payload) {
3022
3172
  throw new ValidationError("full_name is required");
3023
3173
  }
3024
3174
  const phone = payload.whatsapp_phone_number ?? payload.phone;
3025
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE2.test(payload.email))) {
3175
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
3026
3176
  throw new ValidationError("Invalid email address", { email: payload.email });
3027
3177
  }
3028
3178
  validateOptionalPhone(phone);
@@ -3047,7 +3197,7 @@ function validateUpdateSignerPayload(payload) {
3047
3197
  if (payload.full_name !== void 0 && (typeof payload.full_name !== "string" || !payload.full_name.trim())) {
3048
3198
  throw new ValidationError("full_name cannot be empty");
3049
3199
  }
3050
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE2.test(payload.email))) {
3200
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
3051
3201
  throw new ValidationError("Invalid email address", { email: payload.email });
3052
3202
  }
3053
3203
  const phone = payload.whatsapp_phone_number ?? payload.phone;
@@ -3063,7 +3213,7 @@ function validateOptionalDigits(value, field) {
3063
3213
  }
3064
3214
  function validateOptionalPhone(value) {
3065
3215
  if (value === void 0) return;
3066
- if (typeof value !== "string" || !/^\+[1-9]\d{1,14}$/u.test(value)) {
3216
+ if (!isE164PhoneNumber(value)) {
3067
3217
  throw new ValidationError("whatsapp_phone_number must use E.164 format");
3068
3218
  }
3069
3219
  }
@@ -3508,7 +3658,6 @@ var DEFAULT_WEBHOOK_EVENTS = Object.freeze([
3508
3658
  "signer_rejected_document",
3509
3659
  "document_processing_failed"
3510
3660
  ]);
3511
- var EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3512
3661
  var WebhookResource = class extends BaseResource {
3513
3662
  /**
3514
3663
  * Register (or replace) the workspace's single webhook subscription
@@ -3561,7 +3710,7 @@ var WebhookResource = class extends BaseResource {
3561
3710
  throw new ValidationError("Webhook subscription payload is required");
3562
3711
  }
3563
3712
  validateWebhookUrl(payload.url);
3564
- if (!payload.email || !EMAIL_RE3.test(payload.email)) {
3713
+ if (!isEmail(payload.email)) {
3565
3714
  throw new ValidationError("Webhook email must be a valid email address", {
3566
3715
  email: payload.email
3567
3716
  });
@@ -4364,7 +4513,6 @@ function validateTagColor(value) {
4364
4513
  }
4365
4514
 
4366
4515
  // src/resources/authentication.ts
4367
- var EMAIL_RE4 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4368
4516
  var AuthenticationResource = class extends BaseResource {
4369
4517
  publicHttp;
4370
4518
  constructor(http, defaultAccountId, logger, publicHttp) {
@@ -4807,10 +4955,685 @@ var AuthenticationResource = class extends BaseResource {
4807
4955
  return url.toString();
4808
4956
  }
4809
4957
  };
4810
- function assertEmail(value) {
4811
- if (typeof value !== "string" || !EMAIL_RE4.test(value)) {
4812
- throw new ValidationError("email must be a valid email address");
4958
+
4959
+ // src/resources/oauth.ts
4960
+ var import_node_crypto = require("crypto");
4961
+ var PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
4962
+ var AUTHORIZATION_SERVER_PATH = "/.well-known/oauth-authorization-server";
4963
+ var CODE_VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]{43,128}$/u;
4964
+ var OAuthResource = class extends BaseResource {
4965
+ publicHttp;
4966
+ constructor(http, defaultAccountId, logger, publicHttp) {
4967
+ super(http, defaultAccountId, logger);
4968
+ this.publicHttp = withoutCredentials(publicHttp ?? http);
4969
+ }
4970
+ /**
4971
+ * Read this API's protected-resource metadata
4972
+ * (`GET /.well-known/oauth-protected-resource`).
4973
+ *
4974
+ * Served at the API host root — not under `/v1` — and bare, without the
4975
+ * `{ status, message, data }` envelope, as RFC 8615 requires. Use it to
4976
+ * discover which authorization server may issue tokens for this API and
4977
+ * which scopes it accepts.
4978
+ *
4979
+ * Request body: none. Authentication: none.
4980
+ *
4981
+ * @returns The metadata document:
4982
+ * ```jsonc
4983
+ * {
4984
+ * "resource": "https://api.assinafy.com.br",
4985
+ * "authorization_servers": ["https://auth.assinafy.com.br"],
4986
+ * "scopes_supported": [
4987
+ * "documents:read", "documents:write",
4988
+ * "templates:read", "templates:write",
4989
+ * "account:read", "openid", "profile", "email"
4990
+ * ],
4991
+ * "bearer_methods_supported": ["header"]
4992
+ * }
4993
+ * ```
4994
+ * `offline_access` is deliberately absent: it is a request-time signal to
4995
+ * the authorization server, not a permission this API enforces.
4996
+ * @throws {ApiError} If the host does not publish the document.
4997
+ *
4998
+ * @example
4999
+ * ```ts
5000
+ * const metadata = await client.oauth.getProtectedResourceMetadata();
5001
+ * console.log(metadata.authorization_servers[0]);
5002
+ * ```
5003
+ */
5004
+ async getProtectedResourceMetadata() {
5005
+ return this.call(
5006
+ "Failed to fetch OAuth protected-resource metadata",
5007
+ () => this.publicHttp.get(`${this.apiOrigin()}${PROTECTED_RESOURCE_PATH}`)
5008
+ );
5009
+ }
5010
+ /**
5011
+ * Read the authorization server's metadata
5012
+ * (`GET {issuer}/.well-known/oauth-authorization-server`, RFC 8414).
5013
+ *
5014
+ * Every endpoint URL an OAuth client needs comes from here, so nothing has
5015
+ * to be hardcoded. The document is served by the authorization server, a
5016
+ * different host from this API.
5017
+ *
5018
+ * @param issuer - Issuer to read. Defaults to the first entry of
5019
+ * {@link OAuthResource.getProtectedResourceMetadata}, which costs one extra
5020
+ * request — pass the issuer to skip it.
5021
+ * @returns The metadata document:
5022
+ * ```jsonc
5023
+ * {
5024
+ * "issuer": "https://auth.assinafy.com.br",
5025
+ * "authorization_endpoint": "https://auth.assinafy.com.br/oauth/authorize",
5026
+ * "token_endpoint": "https://api.assinafy.com.br/v1/oauth/token",
5027
+ * "revocation_endpoint": "https://api.assinafy.com.br/v1/oauth/revoke",
5028
+ * "userinfo_endpoint": "https://api.assinafy.com.br/v1/oauth/userinfo",
5029
+ * "jwks_uri": "https://auth.assinafy.com.br/.well-known/jwks.json",
5030
+ * "scopes_supported": ["documents:read", "documents:write", "templates:read",
5031
+ * "templates:write", "account:read", "openid",
5032
+ * "profile", "email", "offline_access"],
5033
+ * "response_types_supported": ["code"],
5034
+ * "grant_types_supported": ["authorization_code", "refresh_token"],
5035
+ * "code_challenge_methods_supported": ["S256"],
5036
+ * "token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
5037
+ * "authorization_response_iss_parameter_supported": true,
5038
+ * "client_id_metadata_document_supported": true
5039
+ * }
5040
+ * ```
5041
+ * @throws {ValidationError} If `issuer` is not an absolute `https://` URL,
5042
+ * or the document's own `issuer` disagrees with where it was fetched from
5043
+ * (RFC 8414 §3.3 — a mismatch means the document is not authoritative).
5044
+ * @throws {ApiError} If the authorization server rejects the request.
5045
+ *
5046
+ * @example
5047
+ * ```ts
5048
+ * const as = await client.oauth.getAuthorizationServerMetadata();
5049
+ * console.log(as.authorization_endpoint);
5050
+ * ```
5051
+ */
5052
+ async getAuthorizationServerMetadata(issuer) {
5053
+ const resolved = issuer ?? await this.defaultIssuer();
5054
+ const base = assertHttpsUrl(resolved, "issuer").replace(/\/+$/u, "");
5055
+ const metadata = await this.call(
5056
+ "Failed to fetch OAuth authorization-server metadata",
5057
+ () => this.publicHttp.get(`${base}${AUTHORIZATION_SERVER_PATH}`)
5058
+ );
5059
+ if (normaliseIssuer(metadata?.issuer) !== normaliseIssuer(base)) {
5060
+ throw new ValidationError(
5061
+ "Authorization-server metadata issuer does not match the requested issuer",
5062
+ { expected: base, received: metadata?.issuer ?? null }
5063
+ );
5064
+ }
5065
+ return metadata;
4813
5066
  }
5067
+ /**
5068
+ * Mint a PKCE pair and a `state`, then build the consent URL to send the
5069
+ * user's browser to (`GET {authorization_endpoint}`).
5070
+ *
5071
+ * Call this once per connection attempt and keep the whole returned object
5072
+ * in the user's session: reusing a verifier or a `state` across attempts
5073
+ * defeats both PKCE and CSRF protection. Navigate the browser to `url` with
5074
+ * a full page load — an `fetch`/XHR cannot show a consent screen.
5075
+ *
5076
+ * PKCE is mandatory for confidential applications too, and Assinafy accepts
5077
+ * only the `S256` challenge method.
5078
+ *
5079
+ * @param options - Authorization-request options.
5080
+ * @param options.clientId - The application's `client_id`.
5081
+ * @param options.redirectUri - One of the application's registered redirect
5082
+ * URIs, matched character for character (`…/callback` and `…/callback/` are
5083
+ * different). Must be `https://` and carry no fragment.
5084
+ * @param options.scopes - Permissions to request, e.g.
5085
+ * `['documents:read', 'documents:write', 'offline_access']`. Ask for the
5086
+ * minimum: the user approves all of them or none. Add `offline_access` to
5087
+ * receive a refresh token and `openid` to receive an `id_token`.
5088
+ * @param options.authorizationEndpoint - Skip discovery by supplying the
5089
+ * endpoint yourself. Defaults to the discovered
5090
+ * `authorization_endpoint`.
5091
+ * @param options.issuer - Issuer to discover from, and the value the
5092
+ * callback's `iss` must equal. Defaults to the discovered issuer.
5093
+ * @param options.resource - RFC 8707 resource indicator. Defaults to this
5094
+ * API's origin; pass `null` to omit it. It must match the value sent to the
5095
+ * token endpoint, or the exchange fails with `invalid_target`.
5096
+ * @param options.state - Supply your own CSRF value instead of a generated
5097
+ * one. Must be unique per attempt.
5098
+ * @param options.codeVerifier - Supply your own RFC 7636 verifier (43–128
5099
+ * characters from `A-Z a-z 0-9 - . _ ~`) instead of a generated one.
5100
+ * @param options.nonce - OIDC nonce echoed in the `id_token`. Generated
5101
+ * automatically when `openid` is requested; pass a string to set it or
5102
+ * `null` to omit it.
5103
+ * @param options.prompt - Forwarded as the OIDC `prompt` parameter, e.g.
5104
+ * `'consent'` to force the approval screen again.
5105
+ * @returns The request to store and redirect with:
5106
+ * ```jsonc
5107
+ * {
5108
+ * "url": "https://auth.assinafy.com.br/oauth/authorize?response_type=code&client_id=…&redirect_uri=https%3A%2F%2Fmyapp.com%2Foauth%2Fcallback&scope=documents%3Aread+offline_access&state=8Xv…&code_challenge=E9M…&code_challenge_method=S256&resource=https%3A%2F%2Fapi.assinafy.com.br",
5109
+ * "state": "8Xv2rQ7mJt0aLpKcWn4dZg",
5110
+ * "codeVerifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
5111
+ * "issuer": "https://auth.assinafy.com.br",
5112
+ * "nonce": "n-0S6_WzA2Mj"
5113
+ * }
5114
+ * ```
5115
+ * @throws {ValidationError} If `clientId` is empty, `redirectUri` is not an
5116
+ * absolute `https://` URL without a fragment, `scopes` is empty or contains
5117
+ * a value with whitespace, or a supplied `codeVerifier`/`state` is invalid.
5118
+ * @throws {ApiError} If discovery is needed and fails.
5119
+ *
5120
+ * @example
5121
+ * ```ts
5122
+ * const request = await client.oauth.createAuthorizationUrl({
5123
+ * clientId: process.env.ASSINAFY_CLIENT_ID!,
5124
+ * redirectUri: 'https://myapp.com/oauth/callback',
5125
+ * scopes: ['documents:read', 'documents:write', 'offline_access'],
5126
+ * });
5127
+ * session.oauth = request;
5128
+ * response.redirect(request.url);
5129
+ * ```
5130
+ */
5131
+ async createAuthorizationUrl(options) {
5132
+ assertRecord(options, "authorization options");
5133
+ assertNonEmptyString(options.clientId, "clientId");
5134
+ assertRedirectUri(options.redirectUri);
5135
+ const scope = assertScopes(options.scopes);
5136
+ let endpoint = options.authorizationEndpoint;
5137
+ let issuer = options.issuer;
5138
+ if (endpoint === void 0 || issuer === void 0) {
5139
+ const metadata = await this.getAuthorizationServerMetadata(options.issuer);
5140
+ endpoint ??= metadata.authorization_endpoint;
5141
+ issuer ??= metadata.issuer;
5142
+ }
5143
+ assertHttpsUrl(endpoint, "authorizationEndpoint");
5144
+ assertHttpsUrl(issuer, "issuer");
5145
+ const codeVerifier = options.codeVerifier ?? createCodeVerifier();
5146
+ assertCodeVerifier(codeVerifier);
5147
+ const state = options.state ?? createRandomValue(16);
5148
+ assertNonEmptyString(state, "state");
5149
+ const url = new URL(endpoint);
5150
+ url.searchParams.set("response_type", "code");
5151
+ url.searchParams.set("client_id", options.clientId);
5152
+ url.searchParams.set("redirect_uri", options.redirectUri);
5153
+ url.searchParams.set("scope", scope);
5154
+ url.searchParams.set("state", state);
5155
+ url.searchParams.set("code_challenge", codeChallengeFor(codeVerifier));
5156
+ url.searchParams.set("code_challenge_method", "S256");
5157
+ const { resource } = this.resourceParam(options.resource);
5158
+ if (resource !== void 0) url.searchParams.set("resource", resource);
5159
+ const wantsNonce = options.nonce === void 0 ? options.scopes.includes("openid") : options.nonce !== null;
5160
+ const nonce = wantsNonce ? options.nonce ?? createRandomValue(16) : void 0;
5161
+ if (nonce !== void 0) {
5162
+ assertNonEmptyString(nonce, "nonce");
5163
+ url.searchParams.set("nonce", nonce);
5164
+ }
5165
+ if (options.prompt !== void 0) {
5166
+ assertNonEmptyString(options.prompt, "prompt");
5167
+ url.searchParams.set("prompt", options.prompt);
5168
+ }
5169
+ this.logger.info("Built OAuth authorization URL");
5170
+ const request = {
5171
+ url: url.toString(),
5172
+ state,
5173
+ codeVerifier,
5174
+ issuer
5175
+ };
5176
+ if (nonce !== void 0) request.nonce = nonce;
5177
+ return request;
5178
+ }
5179
+ /**
5180
+ * Validate the authorization response that lands on your redirect URI and
5181
+ * return the code to exchange.
5182
+ *
5183
+ * Checks, in order and before anything else is trusted: `state` equals the
5184
+ * value from {@link OAuthResource.createAuthorizationUrl} (constant-time),
5185
+ * `iss` is present and equals the expected issuer, and only then whether
5186
+ * the server reported an error. A declined consent arrives as
5187
+ * `?error=access_denied`, not as a failed HTTP request.
5188
+ *
5189
+ * The `iss` check is strict because the authorization server advertises
5190
+ * RFC 9207 support and always sends the parameter: a missing `iss` is
5191
+ * treated exactly like a wrong one. Omit `expected.issuer` only if
5192
+ * something between the browser and your handler strips query parameters.
5193
+ *
5194
+ * This performs no network I/O.
5195
+ *
5196
+ * @param params - The callback's query parameters. Accepts an Express-style
5197
+ * `req.query` record, a `URLSearchParams`, a `URL`, a full callback URL
5198
+ * string, or a bare `a=b&c=d` query string.
5199
+ * @param expected - The stored {@link IOAuthAuthorizationRequest} (or any
5200
+ * object carrying its `state` and `issuer`).
5201
+ * @returns The validated response:
5202
+ * ```jsonc
5203
+ * {
5204
+ * "code": "def50200a1b2c3…",
5205
+ * "state": "8Xv2rQ7mJt0aLpKcWn4dZg",
5206
+ * "issuer": "https://auth.assinafy.com.br"
5207
+ * }
5208
+ * ```
5209
+ * @throws {ValidationError} If `state` is missing or does not match, `iss`
5210
+ * is absent or disagrees with the expected issuer, or a successful response
5211
+ * carries no `code`. In every case the response is not yours — stop, do not
5212
+ * exchange.
5213
+ * @throws {OAuthError} If the server returned `error` (e.g.
5214
+ * `access_denied`, `invalid_scope`, `invalid_request`,
5215
+ * `unsupported_response_type`, `invalid_target`).
5216
+ *
5217
+ * @example
5218
+ * ```ts
5219
+ * app.get('/oauth/callback', async (req, res) => {
5220
+ * const stored = req.session.oauth;
5221
+ * const { code } = client.oauth.readAuthorizationCallback(req.query, stored);
5222
+ * const tokens = await client.oauth.exchangeCode({
5223
+ * code,
5224
+ * codeVerifier: stored.codeVerifier,
5225
+ * redirectUri: 'https://myapp.com/oauth/callback',
5226
+ * clientId: process.env.ASSINAFY_CLIENT_ID!,
5227
+ * clientSecret: process.env.ASSINAFY_CLIENT_SECRET,
5228
+ * });
5229
+ * });
5230
+ * ```
5231
+ */
5232
+ readAuthorizationCallback(params, expected) {
5233
+ assertRecord(expected, "expected authorization request");
5234
+ assertNonEmptyString(expected.state, "expected.state");
5235
+ const query = toSearchParams(params);
5236
+ const state = query.get("state");
5237
+ if (state === null || !constantTimeEquals(state, expected.state)) {
5238
+ throw new ValidationError(
5239
+ "OAuth callback state does not match the stored authorization request"
5240
+ );
5241
+ }
5242
+ const issuer = query.get("iss") ?? void 0;
5243
+ if (expected.issuer !== void 0) {
5244
+ if (issuer === void 0 || normaliseIssuer(issuer) !== normaliseIssuer(expected.issuer)) {
5245
+ throw new ValidationError(
5246
+ "OAuth callback issuer is missing or does not match the expected issuer",
5247
+ { expected: expected.issuer, received: issuer ?? null }
5248
+ );
5249
+ }
5250
+ }
5251
+ const error = query.get("error");
5252
+ if (error !== null && error.length > 0) {
5253
+ throw new OAuthError(error, query.get("error_description"), 400, {
5254
+ error,
5255
+ error_description: query.get("error_description")
5256
+ });
5257
+ }
5258
+ const code = query.get("code");
5259
+ if (code === null || code.length === 0) {
5260
+ throw new ValidationError("OAuth callback carries neither a code nor an error");
5261
+ }
5262
+ const result = { code, state };
5263
+ if (issuer !== void 0) result.issuer = issuer;
5264
+ return result;
5265
+ }
5266
+ /**
5267
+ * Exchange an authorization code for tokens
5268
+ * (`POST /oauth/token`, `grant_type=authorization_code`).
5269
+ *
5270
+ * Run this on your server: the code is single-use and expires **60 seconds**
5271
+ * after approval, and a confidential application's secret must never reach
5272
+ * a browser. Every value must match the authorization request exactly, or
5273
+ * the API answers `invalid_grant`.
5274
+ *
5275
+ * @param options - Exchange options.
5276
+ * @param options.code - The code from
5277
+ * {@link OAuthResource.readAuthorizationCallback}.
5278
+ * @param options.codeVerifier - The verifier stored alongside the request.
5279
+ * @param options.redirectUri - The same redirect URI that was authorized.
5280
+ * @param options.clientId - The application's `client_id`.
5281
+ * @param options.clientSecret - The `client_secret`, for confidential
5282
+ * applications only. Public applications omit it and rely on PKCE.
5283
+ * @param options.resource - The same RFC 8707 resource indicator sent to
5284
+ * the authorization endpoint. Defaults to this API's origin; pass `null` to
5285
+ * omit it. A value disagreeing with the authorized one fails with
5286
+ * `invalid_target`.
5287
+ * @returns The token set — a flat object, **not** the API's usual envelope:
5288
+ * ```jsonc
5289
+ * {
5290
+ * "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…",
5291
+ * "token_type": "Bearer",
5292
+ * "expires_in": 3600,
5293
+ * "scope": "documents:read documents:write",
5294
+ * "refresh_token": "def5020088c2…", // only with offline_access
5295
+ * "id_token": "eyJraWQiOiJEQlR0S0…" // only with openid
5296
+ * }
5297
+ * ```
5298
+ * Read `scope` rather than assuming every requested permission was granted.
5299
+ * @throws {ValidationError} If an argument is missing or malformed, or a
5300
+ * `2xx` response carries no `access_token`.
5301
+ * @throws {OAuthError} `invalid_grant` for a spent, expired, replayed or
5302
+ * mismatched code; `invalid_client` for a bad `client_id`/`client_secret`;
5303
+ * `invalid_target` for a `resource` mismatch.
5304
+ *
5305
+ * @example
5306
+ * ```ts
5307
+ * const tokens = await client.oauth.exchangeCode({
5308
+ * code,
5309
+ * codeVerifier: session.oauth.codeVerifier,
5310
+ * redirectUri: 'https://myapp.com/oauth/callback',
5311
+ * clientId: process.env.ASSINAFY_CLIENT_ID!,
5312
+ * clientSecret: process.env.ASSINAFY_CLIENT_SECRET,
5313
+ * });
5314
+ * ```
5315
+ */
5316
+ async exchangeCode(options) {
5317
+ assertRecord(options, "code exchange options");
5318
+ assertNonEmptyString(options.code, "code");
5319
+ assertCodeVerifier(options.codeVerifier);
5320
+ assertRedirectUri(options.redirectUri);
5321
+ return this.requestToken("Failed to exchange the OAuth authorization code", {
5322
+ grant_type: "authorization_code",
5323
+ code: options.code,
5324
+ redirect_uri: options.redirectUri,
5325
+ code_verifier: options.codeVerifier,
5326
+ ...this.clientAuth(options),
5327
+ ...this.resourceParam(options.resource)
5328
+ });
5329
+ }
5330
+ /**
5331
+ * Renew an access token (`POST /oauth/token`, `grant_type=refresh_token`).
5332
+ *
5333
+ * Access tokens last one hour; refresh tokens are available only when
5334
+ * `offline_access` was requested and granted.
5335
+ *
5336
+ * **Refresh tokens rotate.** Every call returns a new one and retires the
5337
+ * one you sent, and a replayed refresh token cannot be told apart from a
5338
+ * stolen one — so the server ends the entire connection and the user must
5339
+ * reconnect. Therefore: persist `refresh_token` from the response before
5340
+ * doing anything else with it, treat a timeout as "it may have succeeded"
5341
+ * and re-read your stored token instead of retrying blindly, and never run
5342
+ * two refreshes concurrently for one connection.
5343
+ *
5344
+ * Refreshing does not extend the connection's 30-day life.
5345
+ *
5346
+ * @param options - Refresh options.
5347
+ * @param options.refreshToken - The current refresh token.
5348
+ * @param options.clientId - The application's `client_id`.
5349
+ * @param options.clientSecret - The `client_secret`, for confidential
5350
+ * applications only.
5351
+ * @param options.resource - RFC 8707 resource indicator. Defaults to this
5352
+ * API's origin; pass `null` to omit it.
5353
+ * @returns A fresh token set, identical in shape to
5354
+ * {@link OAuthResource.exchangeCode}:
5355
+ * ```jsonc
5356
+ * {
5357
+ * "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…",
5358
+ * "token_type": "Bearer",
5359
+ * "expires_in": 3600,
5360
+ * "scope": "documents:read documents:write",
5361
+ * "refresh_token": "def50200f1e2…" // NEW — persist it immediately
5362
+ * }
5363
+ * ```
5364
+ * @throws {ValidationError} If an argument is missing, or a `2xx` response
5365
+ * carries no `access_token`.
5366
+ * @throws {OAuthError} `invalid_grant` when the refresh token was already
5367
+ * used, expired, or the user reconnected with different permissions — ask
5368
+ * the user to reconnect. `invalid_client` for bad client credentials.
5369
+ *
5370
+ * @example
5371
+ * ```ts
5372
+ * const tokens = await client.oauth.refreshToken({
5373
+ * refreshToken: connection.refreshToken,
5374
+ * clientId: process.env.ASSINAFY_CLIENT_ID!,
5375
+ * clientSecret: process.env.ASSINAFY_CLIENT_SECRET,
5376
+ * });
5377
+ * await connection.save({ refreshToken: tokens.refresh_token });
5378
+ * ```
5379
+ */
5380
+ async refreshToken(options) {
5381
+ assertRecord(options, "refresh options");
5382
+ assertNonEmptyString(options.refreshToken, "refreshToken");
5383
+ return this.requestToken("Failed to refresh the OAuth access token", {
5384
+ grant_type: "refresh_token",
5385
+ refresh_token: options.refreshToken,
5386
+ ...this.clientAuth(options),
5387
+ ...this.resourceParam(options.resource)
5388
+ });
5389
+ }
5390
+ /**
5391
+ * Revoke an access or refresh token (`POST /oauth/revoke`, RFC 7009).
5392
+ *
5393
+ * Call this when a user disconnects your app, instead of only deleting your
5394
+ * copy of the token. Revoking a refresh token ends the whole connection.
5395
+ *
5396
+ * Every token outcome answers `200` — unknown, malformed and
5397
+ * already-revoked included — so the endpoint cannot be used to probe
5398
+ * whether a token exists. Only failed client authentication returns `401`.
5399
+ *
5400
+ * @param options - Revocation options.
5401
+ * @param options.token - The access or refresh token to revoke.
5402
+ * @param options.clientId - The application's `client_id`.
5403
+ * @param options.clientSecret - The `client_secret`, for confidential
5404
+ * applications only.
5405
+ * @param options.tokenTypeHint - Optional `access_token` or
5406
+ * `refresh_token` hint that lets the server skip a lookup.
5407
+ * @returns Nothing; resolves once the API acknowledges the request.
5408
+ * Request body:
5409
+ * ```jsonc
5410
+ * {
5411
+ * "token": "def50200f1e2…",
5412
+ * "token_type_hint": "refresh_token",
5413
+ * "client_id": "cli_1a2b3c",
5414
+ * "client_secret": "…"
5415
+ * }
5416
+ * ```
5417
+ * @throws {ValidationError} If `token` or `clientId` is missing, or
5418
+ * `tokenTypeHint` is not one of the two documented values.
5419
+ * @throws {OAuthError} `invalid_client` when client authentication fails.
5420
+ *
5421
+ * @example
5422
+ * ```ts
5423
+ * await client.oauth.revokeToken({
5424
+ * token: connection.refreshToken,
5425
+ * tokenTypeHint: 'refresh_token',
5426
+ * clientId: process.env.ASSINAFY_CLIENT_ID!,
5427
+ * clientSecret: process.env.ASSINAFY_CLIENT_SECRET,
5428
+ * });
5429
+ * ```
5430
+ */
5431
+ async revokeToken(options) {
5432
+ assertRecord(options, "revocation options");
5433
+ assertNonEmptyString(options.token, "token");
5434
+ if (options.tokenTypeHint !== void 0 && options.tokenTypeHint !== "access_token" && options.tokenTypeHint !== "refresh_token") {
5435
+ throw new ValidationError("tokenTypeHint must be access_token or refresh_token");
5436
+ }
5437
+ const body = cleanParams({
5438
+ token: options.token,
5439
+ token_type_hint: options.tokenTypeHint,
5440
+ ...this.clientAuth(options)
5441
+ });
5442
+ try {
5443
+ await this.callVoid(
5444
+ "Failed to revoke the OAuth token",
5445
+ () => this.publicHttp.post("/oauth/revoke", body)
5446
+ );
5447
+ } catch (error) {
5448
+ throw OAuthError.upgrade(error);
5449
+ }
5450
+ }
5451
+ /**
5452
+ * Read the OpenID Connect claims of the user who authorized a token
5453
+ * (`GET /oauth/userinfo`).
5454
+ *
5455
+ * Requires the `openid` scope; `name` additionally requires `profile` and
5456
+ * `email`/`email_verified` require `email`. Per OIDC Core §5.3.2 the
5457
+ * response is a flat claims object, not this API's usual envelope.
5458
+ *
5459
+ * @param accessToken - Token to introspect. Omit to use the credential the
5460
+ * client was constructed with (`token` or `apiKey`).
5461
+ * @returns The claims the granted scopes allow:
5462
+ * ```jsonc
5463
+ * {
5464
+ * "sub": "d6zqpbyog2v3xvxerwn8la94",
5465
+ * "name": "Maria Silva",
5466
+ * "email": "maria@example.com",
5467
+ * "email_verified": true
5468
+ * }
5469
+ * ```
5470
+ * `sub` is the stable user identifier; the rest are `null` when their scope
5471
+ * was not granted.
5472
+ * @throws {ValidationError} If `accessToken` is supplied but empty.
5473
+ * @throws {ApiError} `401` when the token is missing, expired or revoked;
5474
+ * `403` when the `openid` scope was not granted — its `WWW-Authenticate`
5475
+ * header names the scope to reconnect with.
5476
+ *
5477
+ * @example
5478
+ * ```ts
5479
+ * const who = await client.oauth.getUserInfo(tokens.access_token);
5480
+ * console.log(who.sub, who.email);
5481
+ * ```
5482
+ */
5483
+ async getUserInfo(accessToken) {
5484
+ if (accessToken === void 0) {
5485
+ return this.call(
5486
+ "Failed to fetch OAuth userinfo",
5487
+ () => this.http.get("/oauth/userinfo")
5488
+ );
5489
+ }
5490
+ assertNonEmptyString(accessToken, "accessToken");
5491
+ return this.call(
5492
+ "Failed to fetch OAuth userinfo",
5493
+ () => this.publicHttp.get("/oauth/userinfo", {
5494
+ headers: { Authorization: `Bearer ${accessToken}` }
5495
+ })
5496
+ );
5497
+ }
5498
+ /** POST the token endpoint and assert the response actually carries a token. */
5499
+ async requestToken(label, body) {
5500
+ let tokens;
5501
+ try {
5502
+ tokens = await this.call(
5503
+ label,
5504
+ () => this.publicHttp.post("/oauth/token", cleanParams(body))
5505
+ );
5506
+ } catch (error) {
5507
+ throw OAuthError.upgrade(error);
5508
+ }
5509
+ if (typeof tokens?.access_token !== "string" || tokens.access_token.length === 0) {
5510
+ throw new ValidationError(`${label}: the token endpoint returned no access_token`, {
5511
+ response: tokens
5512
+ });
5513
+ }
5514
+ return tokens;
5515
+ }
5516
+ /** `client_secret_post` credentials, omitting the secret for public clients. */
5517
+ clientAuth(options) {
5518
+ assertNonEmptyString(options.clientId, "clientId");
5519
+ if (options.clientSecret !== void 0) {
5520
+ assertNonEmptyString(options.clientSecret, "clientSecret");
5521
+ }
5522
+ return { client_id: options.clientId, client_secret: options.clientSecret };
5523
+ }
5524
+ /**
5525
+ * Resolve the optional RFC 8707 `resource` indicator.
5526
+ *
5527
+ * Defaults to the configured API origin, which is what this API publishes
5528
+ * as its `resource`. A loopback `http://` base URL — the shape used by mock
5529
+ * servers and the packed-consumer smoke test — has no valid resource
5530
+ * identifier, so the parameter is simply omitted rather than rejected; an
5531
+ * explicitly supplied value is still required to be `https`.
5532
+ */
5533
+ resourceParam(resource) {
5534
+ if (resource === void 0) {
5535
+ const origin = this.apiOrigin();
5536
+ return origin.startsWith("https:") ? { resource: origin } : {};
5537
+ }
5538
+ if (resource === null) return {};
5539
+ assertHttpsUrl(resource, "resource");
5540
+ return { resource };
5541
+ }
5542
+ /** Discover which authorization server may issue tokens for this API. */
5543
+ async defaultIssuer() {
5544
+ const metadata = await this.getProtectedResourceMetadata();
5545
+ const issuer = metadata?.authorization_servers?.[0];
5546
+ if (typeof issuer !== "string" || issuer.length === 0) {
5547
+ throw new ValidationError(
5548
+ "Protected-resource metadata lists no authorization server",
5549
+ { metadata }
5550
+ );
5551
+ }
5552
+ return issuer;
5553
+ }
5554
+ /**
5555
+ * Origin of the configured API host.
5556
+ *
5557
+ * The `.well-known` document and the RFC 8707 resource indicator both sit
5558
+ * at the host root, while `baseUrl` points at `/v1`.
5559
+ */
5560
+ apiOrigin() {
5561
+ const baseUrl = this.publicHttp.defaults.baseURL;
5562
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) {
5563
+ throw new ValidationError("The client has no base URL to derive the API origin from");
5564
+ }
5565
+ return new URL(baseUrl).origin;
5566
+ }
5567
+ };
5568
+ function createCodeVerifier() {
5569
+ return (0, import_node_crypto.randomBytes)(32).toString("base64url");
5570
+ }
5571
+ function createRandomValue(bytes) {
5572
+ return (0, import_node_crypto.randomBytes)(bytes).toString("base64url");
5573
+ }
5574
+ function codeChallengeFor(codeVerifier) {
5575
+ return (0, import_node_crypto.createHash)("sha256").update(codeVerifier).digest("base64url");
5576
+ }
5577
+ function assertCodeVerifier(value) {
5578
+ if (typeof value !== "string" || !CODE_VERIFIER_PATTERN.test(value)) {
5579
+ throw new ValidationError(
5580
+ "codeVerifier must be 43-128 characters from A-Z a-z 0-9 - . _ ~"
5581
+ );
5582
+ }
5583
+ }
5584
+ function assertRedirectUri(value) {
5585
+ const uri = assertHttpsUrl(value, "redirectUri");
5586
+ if (uri.includes("#")) {
5587
+ throw new ValidationError("redirectUri must not contain a fragment");
5588
+ }
5589
+ }
5590
+ function assertHttpsUrl(value, label) {
5591
+ if (typeof value !== "string" || value.trim().length === 0) {
5592
+ throw new ValidationError(`${label} must be an absolute https URL`);
5593
+ }
5594
+ let url;
5595
+ try {
5596
+ url = new URL(value);
5597
+ } catch {
5598
+ throw new ValidationError(`${label} must be an absolute https URL`);
5599
+ }
5600
+ if (url.protocol !== "https:") {
5601
+ throw new ValidationError(`${label} must be an absolute https URL`);
5602
+ }
5603
+ return value;
5604
+ }
5605
+ function assertScopes(scopes) {
5606
+ if (!Array.isArray(scopes) || scopes.length === 0) {
5607
+ throw new ValidationError("scopes must be a non-empty array of scope strings");
5608
+ }
5609
+ for (const scope of scopes) {
5610
+ if (typeof scope !== "string" || scope.trim().length === 0 || /\s/u.test(scope)) {
5611
+ throw new ValidationError("each scope must be a non-empty string without whitespace");
5612
+ }
5613
+ }
5614
+ return [...new Set(scopes)].join(" ");
5615
+ }
5616
+ function normaliseIssuer(value) {
5617
+ return typeof value === "string" ? value.replace(/\/+$/u, "") : "";
5618
+ }
5619
+ function constantTimeEquals(left, right) {
5620
+ const a = Buffer.from(left, "utf8");
5621
+ const b = Buffer.from(right, "utf8");
5622
+ return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
5623
+ }
5624
+ function toSearchParams(params) {
5625
+ if (params instanceof URLSearchParams) return params;
5626
+ if (params instanceof URL) return params.searchParams;
5627
+ if (typeof params === "string") {
5628
+ return params.includes("://") ? new URL(params).searchParams : new URLSearchParams(params.replace(/^\?/u, ""));
5629
+ }
5630
+ assertRecord(params, "callback parameters");
5631
+ const search = new URLSearchParams();
5632
+ for (const [key, value] of Object.entries(params)) {
5633
+ const first = Array.isArray(value) ? value[0] : value;
5634
+ if (typeof first === "string") search.set(key, first);
5635
+ }
5636
+ return search;
4814
5637
  }
4815
5638
 
4816
5639
  // src/resources/fields.ts
@@ -5190,8 +6013,6 @@ function validateSignerAccessCode(value) {
5190
6013
  }
5191
6014
 
5192
6015
  // src/resources/signer-documents.ts
5193
- var EMAIL_RE5 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5194
- var E164_RE = /^\+[1-9]\d{1,14}$/u;
5195
6016
  var SignerDocumentsResource = class extends BaseResource {
5196
6017
  constructor(http, defaultAccountId, logger, publicHttp) {
5197
6018
  super(withoutCredentials(publicHttp ?? http), defaultAccountId, logger);
@@ -5848,10 +6669,10 @@ function validateConfirmDataPayload(payload) {
5848
6669
  throw new ValidationError(`${key} must be a string`);
5849
6670
  }
5850
6671
  }
5851
- if (payload.email !== void 0 && (typeof payload.email !== "string" || !EMAIL_RE5.test(payload.email))) {
6672
+ if (payload.email !== void 0 && !isEmail(payload.email)) {
5852
6673
  throw new ValidationError("email must be a valid email address");
5853
6674
  }
5854
- if (payload.whatsapp_phone_number !== void 0 && (typeof payload.whatsapp_phone_number !== "string" || !E164_RE.test(payload.whatsapp_phone_number))) {
6675
+ if (payload.whatsapp_phone_number !== void 0 && !isE164PhoneNumber(payload.whatsapp_phone_number)) {
5855
6676
  throw new ValidationError("whatsapp_phone_number must use E.164 format");
5856
6677
  }
5857
6678
  if (payload.has_accepted_terms !== void 0 && typeof payload.has_accepted_terms !== "boolean") {
@@ -6038,7 +6859,7 @@ function validateNotificationPreferences(preferences) {
6038
6859
  }
6039
6860
 
6040
6861
  // src/support/webhook-verifier.ts
6041
- var import_node_crypto = require("crypto");
6862
+ var import_node_crypto2 = require("crypto");
6042
6863
  var WebhookVerifier = class {
6043
6864
  webhookSecret;
6044
6865
  /**
@@ -6077,10 +6898,10 @@ var WebhookVerifier = class {
6077
6898
  const buf = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
6078
6899
  const provided = signature.trim();
6079
6900
  if (!/^[\da-f]{64}$/i.test(provided)) return false;
6080
- const expected = (0, import_node_crypto.createHmac)("sha256", this.webhookSecret).update(buf).digest();
6901
+ const expected = (0, import_node_crypto2.createHmac)("sha256", this.webhookSecret).update(buf).digest();
6081
6902
  const actual = Buffer.from(provided, "hex");
6082
6903
  try {
6083
- return (0, import_node_crypto.timingSafeEqual)(expected, actual);
6904
+ return (0, import_node_crypto2.timingSafeEqual)(expected, actual);
6084
6905
  } catch {
6085
6906
  return false;
6086
6907
  }
@@ -6165,6 +6986,7 @@ var AssinafyClient = class _AssinafyClient {
6165
6986
  templates;
6166
6987
  tags;
6167
6988
  auth;
6989
+ oauth;
6168
6990
  fields;
6169
6991
  signerDocuments;
6170
6992
  users;
@@ -6271,6 +7093,12 @@ var AssinafyClient = class _AssinafyClient {
6271
7093
  this.logger,
6272
7094
  this.publicAxiosInstance
6273
7095
  );
7096
+ this.oauth = new OAuthResource(
7097
+ this.axiosInstance,
7098
+ void 0,
7099
+ this.logger,
7100
+ this.publicAxiosInstance
7101
+ );
6274
7102
  this.fields = new FieldsResource(this.axiosInstance, this.defaultAccountId, this.logger);
6275
7103
  this.signerDocuments = new SignerDocumentsResource(
6276
7104
  this.publicAxiosInstance,
@@ -6617,11 +7445,20 @@ function normaliseBaseUrl(raw) {
6617
7445
  if (url.protocol !== "https:" && url.protocol !== "http:") {
6618
7446
  throw new ValidationError("baseUrl must use http or https");
6619
7447
  }
7448
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
7449
+ throw new ValidationError(
7450
+ "baseUrl must use https for a remote host; http is only allowed for localhost"
7451
+ );
7452
+ }
6620
7453
  if (url.username || url.password || raw.includes("?") || raw.includes("#")) {
6621
7454
  throw new ValidationError("baseUrl must not contain credentials, a query, or a fragment");
6622
7455
  }
6623
7456
  return url.href.replace(/\/+$/, "");
6624
7457
  }
7458
+ function isLoopbackHost(hostname) {
7459
+ const host = hostname.replace(/^\[|\]$/gu, "");
7460
+ return host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(host);
7461
+ }
6625
7462
  function installCredentialOriginGuard(http, baseURL) {
6626
7463
  const allowedOrigin = new URL(baseURL).origin;
6627
7464
  http.interceptors.request.use((config) => {
@@ -6696,8 +7533,11 @@ function hasIdempotencyKey(headers) {
6696
7533
  DEFAULT_WEBHOOK_EVENTS,
6697
7534
  DocumentResource,
6698
7535
  FieldsResource,
7536
+ MAX_LIST_PAGE_SIZE,
6699
7537
  MAX_UPLOAD_BYTES,
6700
7538
  NetworkError,
7539
+ OAuthError,
7540
+ OAuthResource,
6701
7541
  SDK_USER_AGENT,
6702
7542
  SignerDocumentsResource,
6703
7543
  SignerResource,
@@ -6708,5 +7548,6 @@ function hasIdempotencyKey(headers) {
6708
7548
  WebhookResource,
6709
7549
  WebhookVerifier,
6710
7550
  WorkspaceResource,
6711
- buildAssignmentPayload
7551
+ buildAssignmentPayload,
7552
+ parseWwwAuthenticate
6712
7553
  });